From 3fb6faba893604ef586de4f0307eda22a0a15086 Mon Sep 17 00:00:00 2001 From: yiguo Date: Sat, 27 May 2023 20:26:19 +0800 Subject: [PATCH] init --- .gitignore | 23 +++ LICENSE | 2 +- README.md | 73 ++++++++ build.sh | 29 +++ clash.go | 416 ++++++++++++++++++++++++++++++++++++++++++ controller.go | 16 ++ file.go | 33 ++++ geo.go | 95 ++++++++++ memory.go | 39 ++++ ping.go | 93 ++++++++++ port.go | 27 +++ share.go | 487 ++++++++++++++++++++++++++++++++++++++++++++++++++ uuid.go | 13 ++ xray.go | 71 ++++++++ xray_json.go | 162 +++++++++++++++++ 15 files changed, 1578 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.sh create mode 100644 clash.go create mode 100644 controller.go create mode 100644 file.go create mode 100644 geo.go create mode 100644 memory.go create mode 100644 ping.go create mode 100644 port.go create mode 100644 share.go create mode 100644 uuid.go create mode 100644 xray.go create mode 100644 xray_json.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dcfab8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +*.xcframework/ +*.jar +*.aar +go.mod +go.sum + +main/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 6838ad4..fc30cec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 yiguous +Copyright (c) 2023 XTLS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e90382 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# libxray + +This is an Xray wrapper focusing on improving the experience of [Xray-core](https://github.com/XTLS/Xray-core) mobile development. + +# Features + +### build.sh + +编译脚本。一键生成 xcframework 和 aar。 + +### clash.go + +转换 Clash yaml,Clash.Meta yaml 为 Xray Json。 + +### controller.go + +试验性的 Android Protect(fd)。 + +### file.go + +文件写入。 + +### geo.go + +读取 geosite.data,生成类别名称文件,包含 Attribute。 + +读取 geoip.data,生成类别名称文件。 + +### memory.go + +强制 GC。 + +### ping.go + +测速。 + +### port.go + +获取空闲端口。 + +### share.go + +转换 v2rayN 订阅为 Xray Json。不支持 VMessQRCode。 + +转换 VMessAEAD/VLESS 分享为 Xray Json。 + +### uuid.go + +转换自定义文本为 uuid。 + +### xray_json.go + +Xray 配置的子集,为出口节点添加了 Name 字段,便于 App 内进行解析。 + +### xray.go + +启动和停止 Xray 。 + +# Used By + +[FoXray](https://apps.apple.com/app/foxray/id6448898396) + +# Contributing + +[yiguo](https://yiguo.dev) wrote the original source code. Now it belongs to the Xray Community. + +## Credits + +[Project X](https://github.com/xtls/xray-core) + +[VMessPing](https://github.com/v2fly/vmessping) + +[FreePort](https://github.com/phayes/freeport) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..0889b99 --- /dev/null +++ b/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +go mod init libxray +go mod tidy +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +go get -d golang.org/x/mobile/cmd/gomobile + +build_apple() { + rm -fr *.xcframework + gomobile bind -target ios + mv Libxray.xcframework ios.xcframework + gomobile bind -target macos + mv Libxray.xcframework macos.xcframework + xcodebuild -create-xcframework -framework ios.xcframework/ios-arm64/Libxray.framework -framework ios.xcframework/ios-arm64_x86_64-simulator/Libxray.framework -framework macos.xcframework/macos-arm64_x86_64/Libxray.framework -output Libxray.xcframework +} + +build_android() { + rm -fr *.jar + rm -fr *.aar + gomobile bind -target android -androidapi 28 +} + +echo "will build libxray for $1" +if [ "$1" != "apple" ]; then +build_android +else +build_apple +fi diff --git a/clash.go b/clash.go new file mode 100644 index 0000000..4633a6f --- /dev/null +++ b/clash.go @@ -0,0 +1,416 @@ +package libxray + +import ( + "fmt" + + "encoding/json" + + "gopkg.in/yaml.v3" +) + +type clashYaml struct { + Proxies []clashProxy `yaml:"proxies,omitempty"` +} + +type clashProxy struct { + Name string `yaml:"name,omitempty"` + Type string `yaml:"type,omitempty"` + Server string `yaml:"server,omitempty"` + Port int `yaml:"port,omitempty"` + Uuid string `yaml:"uuid,omitempty"` + Cipher string `yaml:"cipher,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + + Tls bool `yaml:"tls,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Servername string `yaml:"servername,omitempty"` + Sni string `yaml:"sni,omitempty"` + Alpn []string `yaml:"alpn,omitempty"` + + Fingerprint string `yaml:"fingerprint,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` + Flow string `yaml:"flow,omitempty"` + RealityOpts *clashProxyRealityOpts `yaml:"reality-opts,omitempty"` + + Network string `yaml:"network,omitempty"` + Plugin string `yaml:"plugin,omitempty"` + PluginOpts *clashProxyPluginOpts `yaml:"plugin-opts,omitempty"` + WsOpts *clashProxyWsOpts `yaml:"ws-opts,omitempty"` + H2Opts *clashProxyH2Opts `yaml:"h2-opts,omitempty"` + GrpcOpts *clashProxyGrpcOpts `yaml:"grpc-opts,omitempty"` +} + +type clashProxyPluginOpts struct { + Mode string `yaml:"mode,omitempty"` + Tls bool `yaml:"tls,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Host string `yaml:"host,omitempty"` + Path string `yaml:"path,omitempty"` +} + +type clashProxyWsOpts struct { + Path string `yaml:"path,omitempty"` + Headers *clashProxyWsOptsHeaders `yaml:"headers,omitempty"` + MaxEarlyData int `yaml:"max-early-data,omitempty"` + EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"` +} + +type clashProxyWsOptsHeaders struct { + Host string `yaml:"Host,omitempty"` +} + +type clashProxyH2Opts struct { + Host []string `yaml:"host,omitempty"` + Path string `yaml:"path,omitempty"` +} + +type clashProxyGrpcOpts struct { + GrpcServiceName string `yaml:"grpc-service-name,omitempty"` +} + +type clashProxyRealityOpts struct { + PublicKey string `yaml:"public-key,omitempty"` + ShortId string `yaml:"short-id,omitempty"` +} + +func tryConvertClashYaml(text string) (*xrayJson, error) { + clashBytes := []byte(text) + clash := clashYaml{} + + err := yaml.Unmarshal(clashBytes, &clash) + if err != nil { + return nil, err + } + + xray := clash.xrayConfig() + return &xray, nil +} + +func (clash clashYaml) xrayConfig() xrayJson { + var xray xrayJson + + var outbounds []xrayOutbound + for _, proxy := range clash.Proxies { + if outbound, err := proxy.outbound(); err == nil { + outbounds = append(outbounds, *outbound) + } else { + fmt.Println(err) + } + } + xray.Outbounds = outbounds + + return xray +} + +func (proxy clashProxy) outbound() (*xrayOutbound, error) { + switch proxy.Type { + case "ss": + outbound, err := proxy.shadowsocksOutbound() + if err != nil { + return nil, err + } + return outbound, nil + + case "vmess": + outbound, err := proxy.vmessOutbound() + if err != nil { + return nil, err + } + return outbound, nil + + case "vless": + outbound, err := proxy.vlessOutbound() + if err != nil { + return nil, err + } + return outbound, nil + + case "socks5": + outbound, err := proxy.socksOutbound() + if err != nil { + return nil, err + } + return outbound, nil + case "trojan": + outbound, err := proxy.trojanOutbound() + if err != nil { + return nil, err + } + return outbound, nil + } + return nil, fmt.Errorf("unsupport proxy type: %s", proxy.Type) +} + +func (proxy clashProxy) shadowsocksOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "shadowsocks" + outbound.Name = proxy.Name + + var server xrayShadowsocksServer + server.Address = proxy.Server + server.Port = proxy.Port + server.Method = proxy.Cipher + server.Password = proxy.Password + + var settings xrayShadowsocks + settings.Servers = []xrayShadowsocksServer{server} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + if len(proxy.Plugin) != 0 { + if proxy.Plugin != "v2ray-plugin" { + return nil, fmt.Errorf("unsupport ss plugin: obfs") + } + if proxy.PluginOpts == nil { + return nil, fmt.Errorf("unsupport ss plugin-opts: nil") + } + if proxy.PluginOpts.Mode != "websocket" { + return nil, fmt.Errorf("unsupport ss plugin-opts mode: %s", proxy.PluginOpts.Mode) + } + var streamSetting xrayStreamSettings + streamSetting.Network = "websocket" + + var wsSettings xrayWsSettings + if len(proxy.PluginOpts.Path) > 0 { + wsSettings.Path = proxy.PluginOpts.Path + } + if len(proxy.PluginOpts.Host) > 0 { + var headers xrayWsSettingsHeaders + headers.Host = proxy.PluginOpts.Host + wsSettings.Headers = &headers + } + streamSetting.WsSettings = &wsSettings + + if proxy.PluginOpts.Tls { + var tlsSettings xrayTlsSettings + tlsSettings.Fingerprint = proxy.PluginOpts.Fingerprint + tlsSettings.AllowInsecure = proxy.PluginOpts.SkipCertVerify + streamSetting.TlsSettings = &tlsSettings + } + + outbound.StreamSettings = &streamSetting + } + return &outbound, nil +} + +func (proxy clashProxy) vmessOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "vmess" + outbound.Name = proxy.Name + + var user xrayVMessVnextUser + user.Id = proxy.Uuid + user.Security = proxy.Cipher + + var vnext xrayVMessVnext + vnext.Address = proxy.Server + vnext.Port = proxy.Port + vnext.Users = []xrayVMessVnextUser{user} + + var settings xrayVMess + settings.Vnext = []xrayVMessVnext{vnext} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + streamSettings, err := proxy.streamSettings(outbound) + if err != nil { + return nil, err + } + outbound.StreamSettings = streamSettings + + return &outbound, nil +} + +func (proxy clashProxy) vlessOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "vless" + outbound.Name = proxy.Name + + var user xrayVLESSVnextUser + user.Id = proxy.Uuid + user.Flow = proxy.Flow + + var vnext xrayVLESSVnext + vnext.Address = proxy.Server + vnext.Port = proxy.Port + vnext.Users = []xrayVLESSVnextUser{user} + + var settings xrayVLESS + settings.Vnext = []xrayVLESSVnext{vnext} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + streamSettings, err := proxy.streamSettings(outbound) + if err != nil { + return nil, err + } + outbound.StreamSettings = streamSettings + + return &outbound, nil +} + +func (proxy clashProxy) socksOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "socks" + outbound.Name = proxy.Name + + var user xraySocksServerUser + user.User = proxy.Username + user.Pass = proxy.Password + + var server xraySocksServer + server.Address = proxy.Server + server.Port = proxy.Port + server.Users = []xraySocksServerUser{user} + + var settings xraySocks + settings.Servers = []xraySocksServer{server} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + streamSettings, err := proxy.streamSettings(outbound) + if err != nil { + return nil, err + } + outbound.StreamSettings = streamSettings + + return &outbound, nil +} + +func (proxy clashProxy) trojanOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "trojan" + outbound.Name = proxy.Name + + var server xrayTrojanServer + server.Address = proxy.Server + server.Port = proxy.Port + server.Password = proxy.Password + + var settings xrayTrojan + settings.Servers = []xrayTrojanServer{server} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + streamSettings, err := proxy.streamSettings(outbound) + if err != nil { + return nil, err + } + outbound.StreamSettings = streamSettings + + return &outbound, nil +} + +func (proxy clashProxy) streamSettings(outbound xrayOutbound) (*xrayStreamSettings, error) { + var streamSettings xrayStreamSettings + if len(proxy.Network) == 0 { + streamSettings.Network = "tcp" + } else { + streamSettings.Network = proxy.Network + } + + switch streamSettings.Network { + case "ws": + if proxy.WsOpts != nil { + var wsSettings xrayWsSettings + if proxy.WsOpts.Headers != nil { + var headers xrayWsSettingsHeaders + headers.Host = proxy.WsOpts.Headers.Host + wsSettings.Headers = &headers + } + + wsSettings.Path = proxy.WsOpts.Path + + if proxy.WsOpts.MaxEarlyData > 0 { + return nil, fmt.Errorf("unsupport ws-opts max-early-data: %d", proxy.WsOpts.MaxEarlyData) + } + streamSettings.WsSettings = &wsSettings + } + case "h2": + if proxy.H2Opts != nil { + var httpSettings xrayHttpSettings + httpSettings.Host = proxy.H2Opts.Host + httpSettings.Path = proxy.H2Opts.Path + + streamSettings.HttpSettings = &httpSettings + } + case "grpc": + if proxy.GrpcOpts != nil { + var grpcSettings xrayGrpcSettings + grpcSettings.ServiceName = proxy.GrpcOpts.GrpcServiceName + + streamSettings.GrpcSettings = &grpcSettings + } + } + proxy.parseSecurity(&streamSettings, outbound) + return &streamSettings, nil +} + +func (proxy clashProxy) parseSecurity(streamSettings *xrayStreamSettings, outbound xrayOutbound) { + var tlsSettings xrayTlsSettings + var realitySettings xrayRealitySettings + + if proxy.Tls { + streamSettings.Security = "tls" + if proxy.SkipCertVerify { + tlsSettings.AllowInsecure = true + } + } + if proxy.RealityOpts != nil { + streamSettings.Security = "reality" + realitySettings.PublicKey = proxy.RealityOpts.PublicKey + realitySettings.ShortId = proxy.RealityOpts.ShortId + } + if len(proxy.Servername) > 0 { + tlsSettings.ServerName = proxy.Servername + realitySettings.ServerName = proxy.Servername + } + if len(proxy.Sni) > 0 { + tlsSettings.ServerName = proxy.Sni + realitySettings.ServerName = proxy.Sni + } + if len(proxy.Alpn) > 0 { + tlsSettings.Alpn = proxy.Alpn + } + if len(proxy.Fingerprint) > 0 { + tlsSettings.Fingerprint = proxy.Fingerprint + realitySettings.Fingerprint = proxy.Fingerprint + } + if len(proxy.ClientFingerprint) > 0 { + tlsSettings.Fingerprint = proxy.ClientFingerprint + realitySettings.Fingerprint = proxy.ClientFingerprint + } + + if outbound.Protocol == "trojan" && len(streamSettings.Security) == 0 { + streamSettings.Security = "tls" + } + + switch streamSettings.Security { + case "tls": + streamSettings.TlsSettings = &tlsSettings + case "reality": + streamSettings.RealitySettings = &realitySettings + } +} diff --git a/controller.go b/controller.go new file mode 100644 index 0000000..45ccda3 --- /dev/null +++ b/controller.go @@ -0,0 +1,16 @@ +package libxray + +import ( + xinternet "github.com/xtls/xray-core/transport/internet" +) + +type DialerController interface { + FdCallback(int) bool +} + +func RegisterDialerController(controller DialerController) { + xinternet.RegisterDialerController(func(network, address string, fd uintptr) error { + controller.FdCallback(int(fd)) + return nil + }) +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..08e328d --- /dev/null +++ b/file.go @@ -0,0 +1,33 @@ +package libxray + +import ( + "os" +) + +func writeBytes(bytes []byte, path string) error { + fi, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0664) + if err != nil { + return err + } + defer fi.Close() + + _, err = fi.Write(bytes) + if err != nil { + return err + } + return nil +} + +func writeText(text string, path string) error { + fi, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0664) + if err != nil { + return err + } + defer fi.Close() + + _, err = fi.WriteString(text) + if err != nil { + return err + } + return nil +} diff --git a/geo.go b/geo.go new file mode 100644 index 0000000..9931ad1 --- /dev/null +++ b/geo.go @@ -0,0 +1,95 @@ +package libxray + +import ( + "fmt" + "path" + "strconv" + "strings" + "time" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/platform/filesystem" + "google.golang.org/protobuf/proto" +) + +func LoadGeoData(dir string) string { + if err := loadGeoSite(dir); err != nil { + return err.Error() + } + if err := loadGeoIP(dir); err != nil { + return err.Error() + } + ts := time.Now().Unix() + tsText := strconv.FormatInt(ts, 10) + tsPath := path.Join(dir, "timestamp.txt") + if err := writeText(tsText, tsPath); err != nil { + return err.Error() + } + return "" +} + +func loadGeoSite(dir string) error { + datPath := path.Join(dir, "geosite.dat") + txtPath := path.Join(dir, "geosite.txt") + geositeBytes, err := filesystem.ReadFile(datPath) + if err != nil { + return err + } + var geositeList router.GeoSiteList + if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { + return err + } + + var countries []string + for _, site := range geositeList.Entry { + countries = append(countries, site.CountryCode) + for _, domain := range site.Domain { + for _, attribute := range domain.Attribute { + attr := fmt.Sprintf("%s@%s", site.CountryCode, attribute.Key) + if !containsString(countries, attr) { + countries = append(countries, attr) + } + } + } + } + text := strings.Join(countries, "\n") + if err := writeText(text, txtPath); err != nil { + return err + } + + return nil +} + +func loadGeoIP(dir string) error { + datPath := path.Join(dir, "geoip.dat") + txtPath := path.Join(dir, "geoip.txt") + geoipBytes, err := filesystem.ReadFile(datPath) + if err != nil { + return err + } + var geoipList router.GeoIPList + if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { + return err + } + + var countries []string + for _, geoip := range geoipList.Entry { + countries = append(countries, geoip.CountryCode) + } + + text := strings.Join(countries, "\n") + if err := writeText(text, txtPath); err != nil { + return err + } + + return nil +} + +func containsString(slice []string, element string) bool { + for _, e := range slice { + if e == element { + return true + } + } + return false +} diff --git a/memory.go b/memory.go new file mode 100644 index 0000000..7939e84 --- /dev/null +++ b/memory.go @@ -0,0 +1,39 @@ +package libxray + +import ( + "runtime/debug" + "time" + + "github.com/xtls/xray-core/common/platform" +) + +// will be removed when 1.9.0 released + +func forceFree(interval time.Duration) { + go func() { + for { + time.Sleep(interval) + debug.FreeOSMemory() + } + }() +} + +func readForceFreeInterval() int { + const key = "XRAY_MEMORY_FORCEFREE" + const defaultValue = 0 + interval := platform.EnvFlag{ + Name: key, + AltName: platform.NormalizeEnvName(key), + }.GetValueAsInt(defaultValue) + return interval +} + +func initForceFree(maxMemory int64) { + debug.SetGCPercent(10) + debug.SetMemoryLimit(maxMemory) + interval := readForceFreeInterval() + if interval > 0 { + duration := time.Duration(interval) * time.Second + forceFree(duration) + } +} diff --git a/ping.go b/ping.go new file mode 100644 index 0000000..00bb6d9 --- /dev/null +++ b/ping.go @@ -0,0 +1,93 @@ +package libxray + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "time" + + xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/core" +) + +const ( + pingDelayTimeout int64 = 11000 + pingDelayError int64 = 10000 +) + +func Ping(datDir string, config string, timeout int, url string, times int) string { + initEnv(datDir) + server, err := startXray(config) + if err != nil { + return fmt.Sprintf("%d:%s", pingDelayError, err) + } + + if err := server.Start(); err != nil { + return fmt.Sprintf("%d:%s", pingDelayError, err) + } + defer server.Close() + + return measureDelay(server, timeout, url, times) +} + +func measureDelay(inst *core.Instance, timeout int, url string, times int) string { + httpTimeout := time.Second * time.Duration(timeout) + c, err := coreHTTPClient(inst, httpTimeout) + if err != nil { + return fmt.Sprintf("%d:%s", pingDelayError, err) + } + delaySum := int64(0) + count := int64(0) + isValid := false + lastErr := "" + for i := 0; i < times; i++ { + delay, err := coreHTTPRequest(c, url) + if delay != pingDelayTimeout { + delaySum += delay + count += 1 + isValid = true + } else { + lastErr = err.Error() + } + } + if !isValid { + return fmt.Sprintf("%d:%s", pingDelayTimeout, lastErr) + } + return fmt.Sprintf("%d:%s", delaySum/count, lastErr) +} + +func coreHTTPClient(inst *core.Instance, timeout time.Duration) (*http.Client, error) { + if inst == nil { + return nil, errors.New("core instance nil") + } + + tr := &http.Transport{ + DisableKeepAlives: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := xnet.ParseDestination(fmt.Sprintf("%s:%s", network, addr)) + if err != nil { + return nil, err + } + return core.Dial(ctx, inst, dest) + }, + } + + c := &http.Client{ + Transport: tr, + Timeout: timeout, + } + + return c, nil +} + +func coreHTTPRequest(c *http.Client, url string) (int64, error) { + start := time.Now() + req, _ := http.NewRequest("GET", url, nil) + _, err := c.Do(req) + if err != nil { + return pingDelayTimeout, err + } + return time.Since(start).Milliseconds(), nil +} diff --git a/port.go b/port.go new file mode 100644 index 0000000..8cab4ac --- /dev/null +++ b/port.go @@ -0,0 +1,27 @@ +package libxray + +import ( + "fmt" + "net" + "strings" +) + +// https://github.com/phayes/freeport/blob/master/freeport.go +// GetFreePort asks the kernel for free open ports that are ready to use. +func GetFreePorts(count int) string { + var ports []int + for i := 0; i < count; i++ { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return "" + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return "" + } + defer l.Close() + ports = append(ports, l.Addr().(*net.TCPAddr).Port) + } + return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(ports)), ":"), "[]") +} diff --git a/share.go b/share.go new file mode 100644 index 0000000..e4cd510 --- /dev/null +++ b/share.go @@ -0,0 +1,487 @@ +package libxray + +import ( + "fmt" + "strings" + + "encoding/base64" + "encoding/json" + "net/url" + "strconv" + + "github.com/xtls/xray-core/common/platform/filesystem" +) + +func ParseShareText(textPath string, xrayPath string) string { + textBytes, err := filesystem.ReadFile(textPath) + if err != nil { + return err.Error() + } + text := string(textBytes) + text = strings.TrimSpace(text) + + if strings.HasPrefix(text, "{") { + if err = filesystem.CopyFile(xrayPath, textPath); err != nil { + return err.Error() + } + return "" + } + + text = checkWindowsReturn(text) + if strings.HasPrefix(text, "vless://") || strings.HasPrefix(text, "vmess://") || strings.HasPrefix(text, "socks://") || strings.HasPrefix(text, "ss://") || strings.HasPrefix(text, "trojan://") { + xray, err := parsePlainShareText(text) + if err != nil { + return err.Error() + } + err = writeXrayJson(xray, xrayPath) + if err != nil { + return err.Error() + } + } else { + xray, err := tryParse(text) + if err != nil { + return err.Error() + } + err = writeXrayJson(xray, xrayPath) + if err != nil { + return err.Error() + } + } + + return "" +} + +func checkWindowsReturn(text string) string { + if strings.Contains(text, "\r\n") { + text = strings.ReplaceAll(text, "\r\n", "\n") + } + return text +} + +func parsePlainShareText(text string) (*xrayJson, error) { + proxies := strings.Split(text, "\n") + + var xray xrayJson + var outbounds []xrayOutbound + for _, proxy := range proxies { + link, err := url.Parse(proxy) + if err == nil { + var shareLink xrayShareLink + shareLink.text = proxy + shareLink.link = link + if outbound, err := shareLink.outbound(); err == nil { + outbounds = append(outbounds, *outbound) + } else { + fmt.Println(err) + } + } + } + if len(outbounds) == 0 { + return nil, fmt.Errorf("no valid outbound found") + } + xray.Outbounds = outbounds + return &xray, nil +} + +func tryParse(text string) (*xrayJson, error) { + base64Text, err := decodeBase64Text(text) + if err == nil { + cleanText := checkWindowsReturn(base64Text) + return parsePlainShareText(cleanText) + } + return tryConvertClashYaml(text) +} + +func decodeBase64Text(text string) (string, error) { + content, err := base64.StdEncoding.DecodeString(text) + if err == nil { + return string(content), nil + } + if strings.Contains(text, "-") { + text = strings.ReplaceAll(text, "-", "+") + } + if strings.Contains(text, "_") { + text = strings.ReplaceAll(text, "_", "/") + } + missingPadding := len(text) % 4 + if missingPadding != 0 { + padding := strings.Repeat("=", 4-missingPadding) + text += padding + } + content, err = base64.StdEncoding.DecodeString(text) + if err != nil { + return "", err + } + return string(content), nil +} + +func writeXrayJson(xray *xrayJson, xrayPath string) error { + xrayBytes, err := json.Marshal(xray) + if err != nil { + return err + } + + return writeBytes(xrayBytes, xrayPath) +} + +type xrayShareLink struct { + text string + link *url.URL +} + +func (proxy xrayShareLink) outbound() (*xrayOutbound, error) { + switch proxy.link.Scheme { + case "ss": + outbound, err := proxy.shadowsocksOutbound() + if err != nil { + return nil, err + } + return outbound, nil + + case "vmess": + outbound, err := proxy.vmessOutbound() + if err != nil { + return nil, err + } + return outbound, nil + + case "vless": + outbound, err := proxy.vlessOutbound() + if err != nil { + return nil, err + } + return outbound, nil + + case "socks": + outbound, err := proxy.socksOutbound() + if err != nil { + return nil, err + } + return outbound, nil + case "trojan": + outbound, err := proxy.trojanOutbound() + if err != nil { + return nil, err + } + return outbound, nil + } + return nil, fmt.Errorf("unsupport link type: %s", proxy.link.Scheme) +} + +func (proxy xrayShareLink) shadowsocksOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "shadowsocks" + outbound.Name = proxy.link.Fragment + + var server xrayShadowsocksServer + server.Address = proxy.link.Hostname() + port, err := strconv.Atoi(proxy.link.Port()) + if err != nil { + return nil, err + } + server.Port = port + + user := proxy.link.User.String() + passwordText, err := decodeBase64Text(user) + if err != nil { + return nil, err + } + pwConfig := strings.Split(passwordText, ":") + if len(pwConfig) != 2 { + return nil, fmt.Errorf("unsupport link shadowsocks password: %s", passwordText) + } + server.Method = pwConfig[0] + server.Password = pwConfig[1] + + var settings xrayShadowsocks + settings.Servers = []xrayShadowsocksServer{server} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + outbound.StreamSettings = proxy.streamSettings(proxy.link.Query()) + + return &outbound, nil +} + +func (proxy xrayShareLink) vmessOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "vmess" + outbound.Name = proxy.link.Fragment + + query := proxy.link.Query() + + var user xrayVMessVnextUser + user.Id = proxy.link.User.String() + security := query.Get("encryption") + if len(security) > 0 { + user.Security = security + } + + var vnext xrayVMessVnext + vnext.Address = proxy.link.Hostname() + port, err := strconv.Atoi(proxy.link.Port()) + if err != nil { + return nil, err + } + vnext.Port = port + vnext.Users = []xrayVMessVnextUser{user} + + var settings xrayVMess + settings.Vnext = []xrayVMessVnext{vnext} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + outbound.StreamSettings = proxy.streamSettings(query) + + return &outbound, nil +} + +func (proxy xrayShareLink) vlessOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "vless" + outbound.Name = proxy.link.Fragment + + query := proxy.link.Query() + + var user xrayVLESSVnextUser + user.Id = proxy.link.User.String() + flow := query.Get("flow") + if len(flow) > 0 { + user.Flow = flow + } + + var vnext xrayVLESSVnext + vnext.Address = proxy.link.Hostname() + port, err := strconv.Atoi(proxy.link.Port()) + if err != nil { + return nil, err + } + vnext.Port = port + vnext.Users = []xrayVLESSVnextUser{user} + + var settings xrayVLESS + settings.Vnext = []xrayVLESSVnext{vnext} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + outbound.StreamSettings = proxy.streamSettings(query) + + return &outbound, nil +} + +func (proxy xrayShareLink) socksOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "socks" + outbound.Name = proxy.link.Fragment + + userPassword := proxy.link.User.String() + passwordText, err := decodeBase64Text(userPassword) + if err != nil { + return nil, err + } + pwConfig := strings.Split(passwordText, ":") + if len(pwConfig) != 2 { + return nil, fmt.Errorf("unsupport link socks user password: %s", passwordText) + } + var user xraySocksServerUser + user.User = pwConfig[0] + user.Pass = pwConfig[1] + + var server xraySocksServer + server.Address = proxy.link.Hostname() + port, err := strconv.Atoi(proxy.link.Port()) + if err != nil { + return nil, err + } + server.Port = port + server.Users = []xraySocksServerUser{user} + + var settings xraySocks + settings.Servers = []xraySocksServer{server} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + outbound.StreamSettings = proxy.streamSettings(proxy.link.Query()) + + return &outbound, nil +} + +func (proxy xrayShareLink) trojanOutbound() (*xrayOutbound, error) { + var outbound xrayOutbound + outbound.Protocol = "trojan" + outbound.Name = proxy.link.Fragment + + var server xrayTrojanServer + server.Address = proxy.link.Hostname() + port, err := strconv.Atoi(proxy.link.Port()) + if err != nil { + return nil, err + } + server.Port = port + server.Password = proxy.link.User.String() + + var settings xrayTrojan + settings.Servers = []xrayTrojanServer{server} + + setttingsBytes, err := json.Marshal(settings) + if err != nil { + return nil, err + } + outbound.Settings = (*json.RawMessage)(&setttingsBytes) + + outbound.StreamSettings = proxy.streamSettings(proxy.link.Query()) + + return &outbound, nil +} + +func (proxy xrayShareLink) streamSettings(query url.Values) *xrayStreamSettings { + var streamSettings xrayStreamSettings + if len(query) == 0 { + return &streamSettings + } + network := query.Get("type") + if len(network) == 0 { + streamSettings.Network = "tcp" + } else { + streamSettings.Network = network + } + + switch streamSettings.Network { + case "tcp": + headerType := query.Get("headerType") + if headerType == "http" { + var request xrayTcpSettingsHeaderRequest + path := query.Get("path") + if len(path) > 0 { + request.Path = strings.Split(path, ",") + } + host := query.Get("host") + if len(host) > 0 { + var headers xrayTcpSettingsHeaderRequestHeaders + headers.Host = strings.Split(host, ",") + request.Headers = &headers + } + var header xrayTcpSettingsHeader + header.Type = headerType + header.Request = &request + + var tcpSettings xrayTcpSettings + tcpSettings.Header = &header + + streamSettings.TcpSettings = &tcpSettings + } + case "kcp": + var kcpSettings xrayKcpSettings + headerType := query.Get("headerType") + if len(headerType) > 0 { + var header xrayFakeHeader + header.Type = headerType + kcpSettings.Header = &header + } + seed := query.Get("seed") + kcpSettings.Seed = seed + + streamSettings.KcpSettings = &kcpSettings + case "ws": + var wsSettings xrayWsSettings + path := query.Get("path") + wsSettings.Path = path + host := query.Get("host") + if len(host) > 0 { + var headers xrayWsSettingsHeaders + headers.Host = host + wsSettings.Headers = &headers + } + + streamSettings.WsSettings = &wsSettings + case "grpc": + var grcpSettings xrayGrpcSettings + serviceName := query.Get("serviceName") + grcpSettings.ServiceName = serviceName + mode := query.Get("mode") + grcpSettings.MultiMode = mode == "multi" + + streamSettings.GrpcSettings = &grcpSettings + case "quic": + var quicSettings xrayQuicSettings + headerType := query.Get("headerType") + if len(headerType) > 0 { + var header xrayFakeHeader + header.Type = headerType + quicSettings.Header = &header + } + quicSecurity := query.Get("quicSecurity") + quicSettings.Security = quicSecurity + key := query.Get("key") + quicSettings.Key = key + + streamSettings.QuicSettings = &quicSettings + case "http": + var httpSettings xrayHttpSettings + host := query.Get("host") + httpSettings.Host = strings.Split(host, ",") + path := query.Get("path") + httpSettings.Path = path + + streamSettings.HttpSettings = &httpSettings + } + + proxy.parseSecurity(query, &streamSettings) + + return &streamSettings +} + +func (proxy xrayShareLink) parseSecurity(query url.Values, streamSettings *xrayStreamSettings) { + var tlsSettings xrayTlsSettings + var realitySettings xrayRealitySettings + + fp := query.Get("fp") + tlsSettings.Fingerprint = fp + realitySettings.Fingerprint = fp + + sni := query.Get("sni") + tlsSettings.ServerName = sni + realitySettings.ServerName = sni + + alpn := query.Get("alpn") + if len(alpn) > 0 { + tlsSettings.Alpn = strings.Split(alpn, ",") + } + + pbk := query.Get("pbk") + realitySettings.PublicKey = pbk + sid := query.Get("sid") + realitySettings.ShortId = sid + spx := query.Get("spx") + realitySettings.SpiderX = spx + + security := query.Get("security") + if len(security) == 0 { + streamSettings.Security = "none" + } else { + streamSettings.Security = security + } + + switch streamSettings.Security { + case "tls": + streamSettings.TlsSettings = &tlsSettings + case "reality": + streamSettings.RealitySettings = &realitySettings + } +} diff --git a/uuid.go b/uuid.go new file mode 100644 index 0000000..e050aae --- /dev/null +++ b/uuid.go @@ -0,0 +1,13 @@ +package libxray + +import ( + "github.com/xtls/xray-core/common/uuid" +) + +func CustomUUID(str string) string { + id, err := uuid.ParseString(str) + if err != nil { + return err.Error() + } + return id.String() +} diff --git a/xray.go b/xray.go new file mode 100644 index 0000000..4c4b01a --- /dev/null +++ b/xray.go @@ -0,0 +1,71 @@ +package libxray + +import ( + "os" + "runtime/debug" + + "github.com/xtls/xray-core/common/cmdarg" + "github.com/xtls/xray-core/core" + _ "github.com/xtls/xray-core/main/distro/all" +) + +var ( + coreServer *core.Instance +) + +func startXray(configFile string) (*core.Instance, error) { + file := cmdarg.Arg{configFile} + config, err := core.LoadConfig("json", file) + if err != nil { + return nil, err + } + + server, err := core.New(config) + if err != nil { + return nil, err + } + + return server, nil +} + +func initEnv(datDir string) { + os.Setenv("xray.location.asset", datDir) +} + +func setMaxMemory(maxMemory int64) { + os.Setenv("XRAY_MEMORY_FORCEFREE", "1") + initForceFree(maxMemory) +} + +func RunXray(datDir string, config string, maxMemory int64) string { + initEnv(datDir) + if maxMemory > 0 { + setMaxMemory(maxMemory) + } + coreServer, err := startXray(config) + if err != nil { + return err.Error() + } + + if err := coreServer.Start(); err != nil { + return err.Error() + } + + debug.FreeOSMemory() + return "" +} + +func StopXray() string { + if coreServer != nil { + err := coreServer.Close() + coreServer = nil + if err != nil { + return err.Error() + } + } + return "" +} + +func XrayVersion() string { + return core.Version() +} diff --git a/xray_json.go b/xray_json.go new file mode 100644 index 0000000..9ebd065 --- /dev/null +++ b/xray_json.go @@ -0,0 +1,162 @@ +package libxray + +import ( + "encoding/json" +) + +type xrayJson struct { + Outbounds []xrayOutbound `json:"outbounds,omitempty"` +} + +type xrayOutbound struct { + Name string `json:"name,omitempty"` + Protocol string `json:"protocol,omitempty"` + Settings *json.RawMessage `json:"settings,omitempty"` + StreamSettings *xrayStreamSettings `json:"streamSettings,omitempty"` +} + +type xrayShadowsocks struct { + Servers []xrayShadowsocksServer `json:"servers,omitempty"` +} + +type xrayShadowsocksServer struct { + Address string `json:"address,omitempty"` + Port int `json:"port,omitempty"` + Method string `json:"method,omitempty"` + Password string `json:"password,omitempty"` +} + +type xraySocks struct { + Servers []xraySocksServer `json:"servers,omitempty"` +} + +type xraySocksServer struct { + Address string `json:"address,omitempty"` + Port int `json:"port,omitempty"` + Users []xraySocksServerUser `json:"users,omitempty"` +} + +type xraySocksServerUser struct { + User string `json:"user,omitempty"` + Pass string `json:"pass,omitempty"` +} + +type xrayTrojan struct { + Servers []xrayTrojanServer `json:"servers,omitempty"` +} + +type xrayTrojanServer struct { + Address string `json:"address,omitempty"` + Port int `json:"port,omitempty"` + Password string `json:"password,omitempty"` +} + +type xrayVLESS struct { + Vnext []xrayVLESSVnext `json:"vnext,omitempty"` +} + +type xrayVLESSVnext struct { + Address string `json:"address,omitempty"` + Port int `json:"port,omitempty"` + Users []xrayVLESSVnextUser `json:"users,omitempty"` +} + +type xrayVLESSVnextUser struct { + Id string `json:"id,omitempty"` + Flow string `json:"flow,omitempty"` +} + +type xrayVMess struct { + Vnext []xrayVMessVnext `json:"vnext,omitempty"` +} + +type xrayVMessVnext struct { + Address string `json:"address,omitempty"` + Port int `json:"port,omitempty"` + Users []xrayVMessVnextUser `json:"users,omitempty"` +} + +type xrayVMessVnextUser struct { + Id string `json:"id,omitempty"` + Security string `json:"security,omitempty"` +} + +type xrayStreamSettings struct { + Network string `json:"network,omitempty"` + Security string `json:"security,omitempty"` + TlsSettings *xrayTlsSettings `json:"tlsSettings,omitempty"` + RealitySettings *xrayRealitySettings `json:"realitySettings,omitempty"` + TcpSettings *xrayTcpSettings `json:"tcpSettings,omitempty"` + KcpSettings *xrayKcpSettings `json:"kcpSettings,omitempty"` + WsSettings *xrayWsSettings `json:"wsSettings,omitempty"` + HttpSettings *xrayHttpSettings `json:"httpSettings,omitempty"` + QuicSettings *xrayQuicSettings `json:"quicSettings,omitempty"` + GrpcSettings *xrayGrpcSettings `json:"grpcSettings,omitempty"` +} + +type xrayTlsSettings struct { + ServerName string `json:"serverName,omitempty"` + AllowInsecure bool `json:"allowInsecure,omitempty"` + Alpn []string `json:"alpn,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` +} + +type xrayRealitySettings struct { + Fingerprint string `json:"fingerprint,omitempty"` + ServerName string `json:"serverName,omitempty"` + PublicKey string `json:"publicKey,omitempty"` + ShortId string `json:"shortId,omitempty"` + SpiderX string `json:"spiderX,omitempty"` +} + +type xrayTcpSettings struct { + Header *xrayTcpSettingsHeader `json:"header,omitempty"` +} + +type xrayTcpSettingsHeader struct { + Type string `json:"type,omitempty"` + Request *xrayTcpSettingsHeaderRequest `json:"request,omitempty"` +} + +type xrayTcpSettingsHeaderRequest struct { + Path []string `json:"path,omitempty"` + Headers *xrayTcpSettingsHeaderRequestHeaders `json:"headers,omitempty"` +} + +type xrayTcpSettingsHeaderRequestHeaders struct { + Host []string `json:"Host,omitempty"` +} + +type xrayFakeHeader struct { + Type string `json:"type,omitempty"` +} + +type xrayKcpSettings struct { + Header *xrayFakeHeader `json:"header,omitempty"` + Seed string `json:"seed,omitempty"` +} + +type xrayWsSettings struct { + Path string `json:"path,omitempty"` + Headers *xrayWsSettingsHeaders `json:"headers,omitempty"` +} + +type xrayWsSettingsHeaders struct { + Host string `json:"Host,omitempty"` +} + +type xrayHttpSettings struct { + Host []string `json:"host,omitempty"` + Path string `json:"path,omitempty"` +} + +type xrayQuicSettings struct { + Security string `json:"security,omitempty"` + Key string `json:"key,omitempty"` + Header *xrayFakeHeader `json:"header,omitempty"` +} + +type xrayGrpcSettings struct { + ServiceName string `json:"serviceName,omitempty"` + MultiMode bool `json:"multiMode,omitempty"` +}