├── core ├── routing │ ├── type.go │ ├── mode.go │ ├── struct.go │ └── route.go ├── manage │ ├── list.go │ ├── index_manage.go │ ├── manage.go │ ├── recycle_manage.go │ ├── filter_manage.go │ ├── sub_manage.go │ └── node_manage.go ├── tools.go ├── protocols │ ├── mode.go │ ├── base64.go │ ├── protocols.go │ ├── trojan.go │ ├── socks.go │ ├── shadowsocksR.go │ ├── shadowsocks.go │ ├── field │ │ └── field.go │ ├── vmess.go │ ├── vmessAEAD.go │ ├── vless.go │ └── parse.go ├── setting │ ├── key │ │ └── key.go │ ├── routing.go │ ├── dns.go │ ├── test.go │ ├── init.go │ ├── base.go │ └── alias.go ├── node │ ├── str.go │ ├── node.go │ └── filter.go ├── filepath.go ├── sub │ ├── decode.go │ ├── option.go │ ├── sub.go │ └── net.go └── key.go ├── .github └── ISSUE_TEMPLATE │ ├── feature.md │ ├── help.md │ └── bug.md ├── cmd ├── help │ ├── recycle.txt │ ├── alias.txt │ ├── filter.txt │ ├── help.go │ ├── routing.txt │ ├── sub.txt │ ├── node.txt │ ├── help.txt │ └── setting.txt ├── xray.go ├── shell.go ├── recycle.go ├── filter.go ├── alias.go ├── subscribe.go ├── routing.go ├── setting.go └── node.go ├── xray ├── test_config.go ├── tool.go ├── check.go ├── service.go └── config.go ├── Txray.go ├── go.mod ├── log └── log.go └── README.md /core/routing/type.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | type Type string 4 | 5 | const ( 6 | TypeProxy Type = "Proxy" 7 | TypeDirect Type = "Direct" 8 | TypeBlock Type = "Block" 9 | ) 10 | -------------------------------------------------------------------------------- /core/manage/list.go: -------------------------------------------------------------------------------- 1 | package manage 2 | 3 | func HasIn(index int, indexList []int) bool { 4 | for _, i := range indexList { 5 | if i == index { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提议一个新功能 3 | about: 为 Txray 提议一个新的功能特性 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | ### 您想提出的功能特性是否和现有问题相关? 12 | 13 | ### 功能特性内容 14 | -------------------------------------------------------------------------------- /cmd/help/recycle.txt: -------------------------------------------------------------------------------- 1 | recycle {commands} ... 2 | 3 | Commands: 4 | {索引式} 查看节点回收站 5 | help 查看帮助 6 | restore {索引式} 恢复节点 7 | clear 清空节点回收站 8 | 9 | PS: 回收站的数据仅运行存在 (仅存储在内存中) 10 | -------------------------------------------------------------------------------- /core/tools.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | // WriteJSON 将对象写入json文件 9 | func WriteJSON(v interface{}, path string) error { 10 | file, e := os.Create(path) 11 | if e != nil { 12 | return e 13 | } 14 | defer file.Close() 15 | jsonEncode := json.NewEncoder(file) 16 | jsonEncode.SetIndent("", "\t") 17 | return jsonEncode.Encode(v) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/help/alias.txt: -------------------------------------------------------------------------------- 1 | alias {commands} ... 2 | 3 | Commands: 4 | 查看命令别名 5 | help 查看帮助 6 | set {别名} {命令组} 开启过滤规则 7 | rm {索引式} 删除命令别名 8 | 9 | 说明: 10 | 1.命令,如 'node' 'node tcping' 'sub update-node' 这样的单条命令。 11 | 2.命令组,形如 'sub update-node | node tcping | run' 这样的多条命令,以 '|' 分隔,顺序执行。 12 | PS:命令组包含命令,即命令组也可以设置单条命令 13 | -------------------------------------------------------------------------------- /core/protocols/mode.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | type Mode string 4 | 5 | const ( 6 | ModeVMess Mode = "VMess" 7 | ModeVMessAEAD Mode = "VMessAEAD" 8 | ModeVLESS Mode = "VLESS" 9 | ModeTrojan Mode = "Trojan" 10 | ModeShadowSocks Mode = "ShadowSocks" 11 | ModeShadowSocksR Mode = "ShadowSocksR" 12 | ModeSocks Mode = "Socks" 13 | ) 14 | 15 | func (m Mode) String() string { 16 | return string(m) 17 | } 18 | -------------------------------------------------------------------------------- /cmd/help/filter.txt: -------------------------------------------------------------------------------- 1 | filter {commands} ... 2 | 3 | Commands: 4 | 查看过滤规则 5 | help 查看帮助 6 | rm {索引式} 删除过滤规则 7 | open {索引式} 开启过滤规则 8 | close {索引式} 关闭过滤规则 9 | add {过滤规则} 添加过滤规则 10 | run [过滤规则] 手动过滤节点,默认使用内置规则 11 | 12 | PS: 过滤规则==> '过滤范围:正则表达式' 13 | 过滤范围可选值 proto:|name:|addr:|port: 分别代表 协议|别名|地址|端口 14 | 默认为 'name:' 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 请求帮助 3 | about: 使用中出现一些问题,但不确定是否由 Txray 产生 4 | title: '' 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 版本、系统及CPU架构 11 | 12 | 1.你在使用什么版本的Txray,例如 ‘Txray-macos-64 v3.0.2’ 13 | 14 | 2.你所使用的操作系统及CPU架构,例如 ‘windows11 x86-64’ 15 | 16 | ### 描述问题 17 | 18 | 19 | ### Txray设置以及相关截图 20 | 贴入Txray设置(终端运行 Txray setting 查看),以及问题相关的截图。 21 | 22 | -------------------------------------------------------------------------------- /cmd/help/help.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | //go:embed help.txt 8 | var Help string 9 | 10 | //go:embed setting.txt 11 | var Setting string 12 | 13 | //go:embed node.txt 14 | var Node string 15 | 16 | //go:embed sub.txt 17 | var Sub string 18 | 19 | //go:embed filter.txt 20 | var Filter string 21 | 22 | //go:embed recycle.txt 23 | var Recycle string 24 | 25 | //go:embed routing.txt 26 | var Routing string 27 | 28 | //go:embed alias.txt 29 | var Alias string 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 提交 3 | about: 提交一个 Txray 的程序问题报告 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 版本、系统及CPU架构 11 | 12 | 1.你在使用什么版本的Txray,例如 ‘Txray-macos-64 v3.0.2’ 13 | 14 | 2.你所使用的操作系统及CPU架构,例如 ‘windows11 x86-64’ 15 | 16 | ### 描述问题 17 | 18 | 19 | 20 | ### 复现问题的步骤 21 | 22 | 23 | ### Txray设置以及相关截图 24 | 贴入Txray设置(终端运行 Txray setting 查看),以及问题相关的截图。 25 | 26 | -------------------------------------------------------------------------------- /cmd/help/routing.txt: -------------------------------------------------------------------------------- 1 | routing {commands} [flags] ... 2 | 3 | Commands: 4 | help 查看帮助 5 | block [索引式] | [flags] 查看或管理禁止路由规则 6 | direct [索引式] | [flags] 查看或管理直连路由规则 7 | proxy [索引式] | [flags] 查看或管理代理路由规则 8 | 9 | block, direct, proxy Flags 10 | -a, --add {规则} 添加路由规则 11 | -r, --rm {索引式} 删除路由规则 12 | -f, --file {path} 从文件导入规则 13 | -c, --clipboard 从剪贴板导入规则 14 | 15 | PS: 规则详情请访问 https://xtls.github.io/config/routing.html#ruleobject 16 | -------------------------------------------------------------------------------- /xray/test_config.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/protocols" 6 | "Txray/log" 7 | "path/filepath" 8 | ) 9 | 10 | // 生成xray-core配置文件 11 | func GenTestConfig(node protocols.Protocol) string { 12 | path := filepath.Join(core.GetConfigDir(), "config.json") 13 | var conf = map[string]interface{}{ 14 | "inbounds": inboundsConfig(), 15 | "outbounds": outboundConfig(node), 16 | } 17 | err := core.WriteJSON(conf, path) 18 | if err != nil { 19 | log.Error(err) 20 | panic(err) 21 | } 22 | return path 23 | } -------------------------------------------------------------------------------- /core/manage/index_manage.go: -------------------------------------------------------------------------------- 1 | package manage 2 | 3 | import "Txray/core/node" 4 | 5 | func (m *Manage) SelectedNode() *node.Node { 6 | return m.GetNode(m.SelectedIndex()) 7 | } 8 | 9 | func (m *Manage) SelectedIndex() int { 10 | return m.Index 11 | } 12 | 13 | func (m *Manage) SetSelectedIndex(index int) int { 14 | m.Index = index 15 | return m.Index 16 | } 17 | 18 | func (m *Manage) SetSelectedIndexByNode(n *node.Node) int { 19 | m.SetSelectedIndex(1) 20 | if n != nil { 21 | m.NodeForEach(func(i int, n1 *node.Node) { 22 | if n == n1 { 23 | m.SetSelectedIndex(i) 24 | return 25 | } 26 | }) 27 | } 28 | return m.Index 29 | } 30 | -------------------------------------------------------------------------------- /core/routing/mode.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | type Mode string 9 | 10 | const ( 11 | ModeIP Mode = "IP" 12 | ModeDomain = "Domain" 13 | ) 14 | 15 | // 判断是IP规则还是域名规则 16 | // IP|Domain 17 | func GetRuleMode(str string) Mode { 18 | if strings.HasPrefix(str, "geoip:") { 19 | return ModeIP 20 | } 21 | if strings.Contains(str, "ip.dat:") { 22 | return ModeIP 23 | } 24 | pattern := `(?:^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})(?:/(?:[1-9]|[1-2][0-9]|3[0-2]){1})?$` 25 | re, _ := regexp.Compile(pattern) 26 | if re.MatchString(str) { 27 | return ModeIP 28 | } 29 | return ModeDomain 30 | } 31 | -------------------------------------------------------------------------------- /cmd/xray.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/core/manage" 5 | "Txray/xray" 6 | "github.com/abiosoft/ishell" 7 | "strconv" 8 | ) 9 | 10 | func InitServiceShell(shell *ishell.Shell) { 11 | // 启动或重启服务 12 | shell.AddCmd(&ishell.Cmd{ 13 | Name: "run", 14 | Help: "启动或重启服务", 15 | Func: func(c *ishell.Context) { 16 | if len(c.Args) == 1 { 17 | xray.Start(c.Args[0]) 18 | } else { 19 | xray.Start(strconv.Itoa(manage.Manager.SelectedIndex())) 20 | } 21 | 22 | }, 23 | }) 24 | // 停止服务 25 | shell.AddCmd(&ishell.Cmd{ 26 | Name: "stop", 27 | Help: "停止服务", 28 | Func: func(c *ishell.Context) { 29 | xray.Stop() 30 | }, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /core/setting/key/key.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | const ( 4 | Socks = "socks" 5 | Http = "http" 6 | UDP = "udp" 7 | Sniffing = "sniffing" 8 | FromLanConn = "from_lan_conn" 9 | Mux = "mux" 10 | AllowInsecure = "allow_insecure" 11 | 12 | RoutingStrategy = "routing.strategy" 13 | RoutingBypass = "routing.bypass" // 绕过局域网和大陆 14 | 15 | DNSPort = "dns.port" 16 | DNSDomestic = "dns.domestic" // 国内dns 17 | DNSForeign = "dns.foreign" // 国外dns 18 | DNSBackup = "dns.backup" //备用dns 19 | 20 | TestURL = "test.url" 21 | TestTimeout = "test.timeout" 22 | TestMinTime = "test.mintime" 23 | 24 | RunBefore = "run_before" 25 | PID = "pid" // 核心进程id 26 | ) 27 | -------------------------------------------------------------------------------- /core/node/str.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | "github.com/mattn/go-runewidth" 6 | "strings" 7 | ) 8 | 9 | // 生成num个相同字符组成的字符串 10 | func RepeatChar(ch byte, num int) string { 11 | return strings.Repeat(string(ch), num) 12 | } 13 | 14 | // 所有字符串中最大宽度 15 | func MaxWidth(str ...string) int { 16 | max := 0 17 | for _, s := range str { 18 | width := runewidth.StringWidth(s) 19 | if width > max { 20 | max = width 21 | } 22 | } 23 | return max 24 | } 25 | 26 | // 添加上下的分割线 27 | func ShowTopBottomSepLine(ch byte, str ...string) { 28 | width := MaxWidth(str...) 29 | fmt.Println(RepeatChar(ch, width)) 30 | fmt.Println(strings.Join(str, "\n")) 31 | fmt.Println(RepeatChar(ch, width)) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/help/sub.txt: -------------------------------------------------------------------------------- 1 | sub {commands} [flags] ... 2 | 3 | Commands: 4 | 查看订阅信息 5 | help 查看帮助 6 | rm {索引式} 删除订阅 7 | add {订阅url} [flags] 添加订阅 8 | mv {索引式} {flags} 修改订阅 9 | update-node [索引式] [flags] 从订阅更新节点, 索引式会忽略是否启用 10 | 11 | add Flags 12 | -r, --remarks {别名} 定义别名 13 | 14 | mv Flags 15 | -u, --url {订阅url} 修改订阅链接 16 | -r, --remarks {别名} 定义别名 17 | --using {y|n} 是否启用此订阅 18 | 19 | update-node Flags 20 | -s, --socks5 [port] 通过本地的socks5代理更新, 默认为设置中的socks5端口 21 | -h, --http [port] 通过本地的http代理更新, 默认为设置中的http端口 22 | -a, --addr {address} 对上面两个参数的补充, 修改代理地址 23 | -------------------------------------------------------------------------------- /cmd/help/node.txt: -------------------------------------------------------------------------------- 1 | node {commands} [flags] ... 2 | 3 | Commands: 4 | [索引式] [flags] 查看节点信息, 默认 'all' 5 | help 查看帮助 6 | tcping 测试节点tcp延迟 7 | sort {0|1|2|3|4|5} 排序方式,分别按{逆转|协议|别名|地址|端口|测试结果}排序 8 | info {索引} 查看单个节点详细信息 9 | rm {索引式} 删除节点 10 | find {关键词} 查找节点(按别名) 11 | add [flags] 添加节点 12 | export [索引式] [flags] 导出节点链接, 默认'all' 13 | 14 | Flags 15 | -d, --desc 降序查看 16 | 17 | add Flags 18 | -l, --link {link} 从链接导入一条节点 19 | -f, --file {path} 从节点链接文件或订阅文件导入节点 20 | -c, --clipboard 从剪贴板读取的节点链接或订阅文本导入节点 21 | 22 | export Flags 23 | -c, --clipboard 导出节点链接到剪贴板 24 | -------------------------------------------------------------------------------- /core/setting/routing.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "Txray/core/setting/key" 5 | "errors" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func RoutingStrategy() string { 10 | return viper.GetString(key.RoutingStrategy) 11 | } 12 | 13 | func SetRoutingStrategy(mode int) error { 14 | switch mode { 15 | case 1: 16 | viper.Set(key.RoutingStrategy, "AsIs") 17 | case 2: 18 | viper.Set(key.RoutingStrategy, "IPIfNonMatch") 19 | case 3: 20 | viper.Set(key.RoutingStrategy, "IPOnDemand") 21 | default: 22 | return errors.New("没有对应的路由策略") 23 | } 24 | return viper.WriteConfig() 25 | } 26 | 27 | func RoutingBypass() bool { 28 | return viper.GetBool(key.RoutingBypass) 29 | } 30 | 31 | func SetRoutingBypass(status bool) error { 32 | viper.Set(key.RoutingBypass, status) 33 | return viper.WriteConfig() 34 | } 35 | -------------------------------------------------------------------------------- /core/filepath.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var ( 9 | DataFile = filepath.Join(GetConfigDir(), "data.json") 10 | SettingFile = filepath.Join(GetConfigDir(), "setting.toml") 11 | RoutingFile = filepath.Join(GetConfigDir(), "routing.json") 12 | LogFile = filepath.Join(GetConfigDir(), "xray_access.log") 13 | ) 14 | 15 | // 获取配置文件所在目录 16 | func GetConfigDir() string { 17 | dir := os.Getenv("TXRAY_HOME") 18 | if dir != "" && IsDir(dir) { 19 | return dir 20 | } 21 | return GetRunPath() 22 | } 23 | 24 | 25 | // 获取当前进程的可执行文件的路径名 26 | func GetRunPath() string { 27 | path, _ := os.Executable() 28 | return filepath.Dir(path) 29 | } 30 | 31 | // 判断路径是否正确且为文件夹 32 | func IsDir(path string) bool { 33 | i, err := os.Stat(path) 34 | if err == nil { 35 | return i.IsDir() 36 | } 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /core/protocols/base64.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "encoding/base64" 5 | "strings" 6 | ) 7 | 8 | func base64Encode(str string) string { 9 | return base64.RawStdEncoding.EncodeToString([]byte(str)) 10 | } 11 | 12 | func base64EncodeWithEq(str string) string { 13 | return base64.StdEncoding.EncodeToString([]byte(str)) 14 | } 15 | 16 | func base64Decode(str string) string { 17 | i := len(str) % 4 18 | switch i { 19 | case 1: 20 | str = str[:len(str)-1] 21 | case 2: 22 | str += "==" 23 | case 3: 24 | str += "=" 25 | } 26 | var data []byte 27 | var err error 28 | if strings.Contains(str, "-") || strings.Contains(str, "_") { 29 | data, err = base64.URLEncoding.DecodeString(str) 30 | } else { 31 | data, err = base64.StdEncoding.DecodeString(str) 32 | } 33 | if err != nil { 34 | return "" 35 | } 36 | return string(data) 37 | } 38 | -------------------------------------------------------------------------------- /core/sub/decode.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func base64Decode(str string) string { 10 | i := len(str) % 4 11 | switch i { 12 | case 1: 13 | str = str[:len(str)-1] 14 | case 2: 15 | str += "==" 16 | case 3: 17 | str += "=" 18 | } 19 | var data []byte 20 | var err error 21 | if strings.Contains(str, "-") || strings.Contains(str, "_") { 22 | data, err = base64.URLEncoding.DecodeString(str) 23 | } else { 24 | data, err = base64.StdEncoding.DecodeString(str) 25 | } 26 | if err != nil { 27 | fmt.Println(err) 28 | } 29 | return string(data) 30 | } 31 | 32 | // 解析订阅文本 33 | func Sub2links(subtext string) []string { 34 | data := base64Decode(subtext) 35 | s := strings.ReplaceAll(data, "\r\n", "\n") 36 | s = strings.ReplaceAll(s, "\r", "\n") 37 | list := strings.Split(strings.TrimRight(s, "\n"), "\n") 38 | return list 39 | } 40 | -------------------------------------------------------------------------------- /core/sub/option.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "Txray/core/setting" 5 | "time" 6 | ) 7 | 8 | type ProxyProtocol int 9 | 10 | const ( 11 | NONE ProxyProtocol = iota 12 | SOCKS 13 | HTTP 14 | ) 15 | 16 | type UpdataOption struct { 17 | Key string 18 | ProxyMode ProxyProtocol 19 | Addr string 20 | Port int 21 | Timeout time.Duration 22 | } 23 | 24 | func (o *UpdataOption) proxyMode() ProxyProtocol { 25 | return o.ProxyMode 26 | } 27 | 28 | func (o *UpdataOption) addr() string { 29 | if o.Addr == "" { 30 | return "127.0.0.1" 31 | } 32 | return o.Addr 33 | } 34 | 35 | func (o *UpdataOption) port() int { 36 | if o.Port != 0 { 37 | return o.Port 38 | } 39 | switch o.ProxyMode { 40 | case SOCKS: 41 | return setting.Socks() 42 | case HTTP: 43 | return setting.Http() 44 | } 45 | return o.Port 46 | } 47 | func (o *UpdataOption) timeout() time.Duration { 48 | if o.Timeout == 0 { 49 | return 5 * time.Second 50 | } 51 | return o.Timeout 52 | } 53 | -------------------------------------------------------------------------------- /core/setting/dns.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "Txray/core/setting/key" 5 | "errors" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func DNSPort() int { 10 | return viper.GetInt(key.DNSPort) 11 | } 12 | 13 | func SetDNSPort(port int) error { 14 | if port < 0 || port > 65535 { 15 | return errors.New("http端口取值 0~65535") 16 | } 17 | viper.Set(key.DNSPort, port) 18 | return viper.WriteConfig() 19 | } 20 | 21 | func DNSDomestic() string { 22 | return viper.GetString(key.DNSDomestic) 23 | } 24 | 25 | func SetDNSDomestic(dns string) error { 26 | viper.Set(key.DNSDomestic, dns) 27 | return viper.WriteConfig() 28 | } 29 | 30 | func DNSForeign() string { 31 | return viper.GetString(key.DNSForeign) 32 | } 33 | 34 | func SetDNSForeign(dns string) error { 35 | viper.Set(key.DNSForeign, dns) 36 | return viper.WriteConfig() 37 | } 38 | 39 | func DNSBackup() string { 40 | return viper.GetString(key.DNSBackup) 41 | } 42 | 43 | func SetDNSBackup(dns string) error { 44 | viper.Set(key.DNSBackup, dns) 45 | return viper.WriteConfig() 46 | } 47 | -------------------------------------------------------------------------------- /xray/tool.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "Txray/log" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | 12 | // 获取节点代理访问外网的延迟 13 | func TestNode(url string, port int, timeout int) (int, string) { 14 | start := time.Now() 15 | res, e := GetBySocks5Proxy(url, "127.0.0.1", port, time.Duration(timeout)*time.Second) 16 | elapsed := time.Since(start) 17 | if e != nil { 18 | log.Warn(e) 19 | return -1, "Error" 20 | } 21 | result, status := int(float32(elapsed.Nanoseconds())/1e6), res.Status 22 | defer res.Body.Close() 23 | return result, status 24 | } 25 | 26 | // 通过Socks5代理访问网站 27 | func GetBySocks5Proxy(objUrl, proxyAddress string, proxyPort int, timeOut time.Duration) (*http.Response, error) { 28 | proxy := func(_ *http.Request) (*url.URL, error) { 29 | return url.Parse(fmt.Sprintf("socks5://%s:%d", proxyAddress, proxyPort)) 30 | } 31 | transport := &http.Transport{Proxy: proxy} 32 | client := &http.Client{ 33 | Transport: transport, 34 | Timeout: timeOut, 35 | } 36 | return client.Get(objUrl) 37 | } 38 | -------------------------------------------------------------------------------- /core/routing/struct.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/log" 6 | "encoding/json" 7 | "os" 8 | ) 9 | 10 | type routing struct { 11 | Data string `json:"data"` 12 | Mode Mode `json:"mode"` 13 | } 14 | 15 | type Routing struct { 16 | Proxy []*routing `json:"proxy"` 17 | Direct []*routing `json:"direct"` 18 | Block []*routing `json:"block"` 19 | } 20 | 21 | var route *Routing = NewRouting() 22 | 23 | func NewRouting() *Routing { 24 | return &Routing{ 25 | Proxy: make([]*routing, 0), 26 | Direct: make([]*routing, 0), 27 | Block: make([]*routing, 0), 28 | } 29 | } 30 | 31 | func init() { 32 | if _, err := os.Stat(core.RoutingFile); os.IsNotExist(err) { 33 | route.save() 34 | } else { 35 | file, _ := os.Open(core.RoutingFile) 36 | defer file.Close() 37 | err := json.NewDecoder(file).Decode(route) 38 | if err != nil { 39 | log.Error(err) 40 | } 41 | } 42 | } 43 | 44 | func (r *Routing) save() { 45 | err := core.WriteJSON(r, core.RoutingFile) 46 | if err != nil { 47 | log.Error(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/setting/test.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "Txray/core/setting/key" 5 | "errors" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func TestUrl() string { 10 | return viper.GetString(key.TestURL) 11 | } 12 | 13 | func SetTestUrl(url string) error { 14 | viper.Set(key.TestURL, url) 15 | return viper.WriteConfig() 16 | } 17 | 18 | func TestTimeout() int { 19 | return viper.GetInt(key.TestTimeout) 20 | } 21 | 22 | func SetTestTimeout(timeout int) error { 23 | if timeout < 0 { 24 | return errors.New("取值不能小于0") 25 | } 26 | viper.Set(key.TestTimeout, timeout) 27 | return viper.WriteConfig() 28 | } 29 | 30 | func TestMinTime() int { 31 | return viper.GetInt(key.TestMinTime) 32 | } 33 | 34 | func SetTestMinTime(timeout int) error { 35 | if timeout < 0 { 36 | return errors.New("取值不能小于0") 37 | } 38 | viper.Set(key.TestMinTime, timeout) 39 | return viper.WriteConfig() 40 | } 41 | 42 | func RunBefore() string { 43 | return viper.GetString(key.RunBefore) 44 | } 45 | 46 | func SetRunBefore(cmd string) error { 47 | viper.Set(key.RunBefore, cmd) 48 | return viper.WriteConfig() 49 | } 50 | -------------------------------------------------------------------------------- /core/sub/sub.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "Txray/log" 5 | "crypto/md5" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | type Subscirbe struct { 11 | Name string `json:"name"` 12 | Url string `json:"url"` 13 | Using bool `json:"using"` 14 | } 15 | 16 | // New方法 17 | func NewSubscirbe(url, name string) *Subscirbe { 18 | return &Subscirbe{Name: name, Url: url, Using: true} 19 | } 20 | 21 | func (s *Subscirbe) ID() string { 22 | return fmt.Sprintf("%x", md5.Sum([]byte(s.Url))) 23 | } 24 | 25 | func (s *Subscirbe) UpdataNode(opt UpdataOption) []string { 26 | var res *http.Response 27 | var err error 28 | switch opt.proxyMode() { 29 | case SOCKS: 30 | res, err = GetBySocks5Proxy(s.Url, opt.addr(), opt.port(), opt.timeout()) 31 | case HTTP: 32 | res, err = GetByHTTPProxy(s.Url, opt.addr(), opt.port(), opt.timeout()) 33 | default: 34 | res, err = GetNoProxy(s.Url, opt.timeout()) 35 | } 36 | if err != nil { 37 | log.Error(err) 38 | return []string{} 39 | } 40 | log.Info("访问 [", s.Url, "] -- ", res.Status) 41 | text := ReadDate(res) 42 | res.Body.Close() 43 | return Sub2links(text) 44 | } 45 | -------------------------------------------------------------------------------- /Txray.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "Txray/cmd" 5 | "Txray/core" 6 | "Txray/core/setting" 7 | "Txray/core/setting/key" 8 | "Txray/log" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/abiosoft/ishell" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | const ( 17 | version = "v3.0.8" 18 | name = "Txray" 19 | ) 20 | 21 | func init() { 22 | // 初始化日志 23 | absPath := filepath.Join(core.GetConfigDir(), "info.log") 24 | log.Init( 25 | log.GetConsoleZapcore(log.INFO), 26 | log.GetFileZapcore(absPath, log.INFO, 5), 27 | ) 28 | } 29 | 30 | func beforeOfRun(shell *ishell.Shell) { 31 | cmd := viper.GetString(key.RunBefore) 32 | if cmd != "" { 33 | for _, line := range setting.NewAlias("", cmd).GetCmd() { 34 | shell.Process(line...) 35 | } 36 | shell.Print("\n>>> ") 37 | } 38 | } 39 | 40 | func main() { 41 | shell := ishell.New() 42 | cmd.InitShell(shell) 43 | shell.Set("version", version) 44 | shell.Set("name", name) 45 | if len(os.Args) > 1 { 46 | _ = shell.Process(os.Args[1:]...) 47 | } else { 48 | go beforeOfRun(shell) 49 | shell.Printf("%s - Xray Shell Client - %s\n", name, version) 50 | shell.Run() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/help/help.txt: -------------------------------------------------------------------------------- 1 | Commands: 2 | setting 基础设置 使用 'setting help' 查看详细用法 3 | node 节点管理 使用 'node help' 查看详细用法 4 | sub 订阅管理 使用 'sub help' 查看详细用法 5 | routing 路由管理 使用 'routing help' 查看详细用法 6 | filter 节点过滤 使用 'filter help' 查看详细用法 7 | recycle 回收站 使用 'recycle help' 查看详细用法 8 | alias 命令别名 使用 'alias help' 查看详细用法 9 | help, -h 查看帮助信息 10 | version, -v 查看版本 11 | clear 清屏 12 | exit 退出程序 13 | run 启动或重启节点 14 | stop 关闭节点 15 | log 查看运行时xray日志 16 | 17 | Usage: run [索引式] 18 | run [索引式] 默认为上一次运行节点,如果选中多个节点,则选择访问 'setting' 中测试国外URL延迟最小的 19 | 20 | 21 | 说明: 22 | 一、索引式:更简单的批量选择 23 | 1.选择前6个:'1,2,3,4,5,6' 或 '1-3,4-6' 或 '1-6' 或 '-6' 24 | 2.选择第6个及后面的所有:'6-' 25 | 3.选择第6个:'6' 26 | 4.选择所有:'all' 或 '-' 27 | 注意:超出部分会被忽略,'all' 只能单独使用 28 | 29 | 二、[] 和 {}:帮助说明中的中括号和大括号 30 | 1. []: 表示该选项可忽略 31 | 2. {}: 表示该选项为必须,不可忽略 32 | -------------------------------------------------------------------------------- /cmd/help/setting.txt: -------------------------------------------------------------------------------- 1 | setting {commands} 2 | 3 | Commands: 4 | 查看所有设置 5 | help 查看帮助 6 | 7 | socks [port] 设置socks端口 8 | http [port] 设置http端口, 0为关闭http监听 9 | udp [y|n] 是否启用udp转发 10 | sniffing [y|n] 是否启用流量地址监听 11 | from_lan_conn [y|n] 是否启用来自局域网连接 12 | mux [y|n] 是否启用多路复用(下载和看视频时建议关闭) 13 | 14 | dns.port [port] 设置DNS端口 15 | dns.foreign [dns] 设置国外DNS 16 | dns.domestic [dns] 设置国内DNS 17 | dns.backup [dns] 设置国内备用DNS 18 | 19 | routing.strategy {1|2|3} 设置路由策略为{AsIs|IPIfNonMatch|IPOnDemand} 20 | routing.bypass {y|n} 是否绕过局域网及大陆 21 | 22 | test.url [url] 设置外网测试URL 23 | test.timeout [time] 设置外网测试超时时间 (秒) 24 | test.mintime [time] 设置批量测试终止时间 (毫秒) 25 | 26 | run_before [命令组] [flags] 程序启动时执行命令或命令组,可与命令别名搭配 27 | 28 | 29 | run_before Flags 30 | -c, --close 启动时不执行任何命令 31 | 32 | 说明: 33 | 1.命令,如 'node' 'node tcping' 'sub update-node' 这样的单条命令。 34 | 2.命令组,形如 'sub update-node | node tcping | run' 这样的多条命令,以 '|' 分隔,顺序执行。 35 | PS:命令组包含命令,即命令组也可以设置单条命令 -------------------------------------------------------------------------------- /core/manage/manage.go: -------------------------------------------------------------------------------- 1 | package manage 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/node" 6 | "Txray/core/sub" 7 | "Txray/log" 8 | "encoding/json" 9 | "os" 10 | ) 11 | 12 | type Manage struct { 13 | Subs []*sub.Subscirbe `json:"subs"` 14 | Index int `json:"index"` 15 | NodeList []*node.Node `json:"nodes"` 16 | Filter []*node.NodeFilter `json:"filter"` 17 | RecycleNodeList []*node.Node `json:"-"` 18 | } 19 | 20 | var Manager *Manage 21 | 22 | // 初始化 23 | func init() { 24 | Manager = NewManage() 25 | if _, err := os.Stat(core.DataFile); os.IsNotExist(err) { 26 | Manager.Save() 27 | } else { 28 | file, _ := os.Open(core.DataFile) 29 | defer file.Close() 30 | err := json.NewDecoder(file).Decode(Manager) 31 | if err != nil { 32 | log.Error(err) 33 | } 34 | Manager.NodeForEach(func(i int, n *node.Node) { 35 | n.ParseData() 36 | }) 37 | } 38 | } 39 | 40 | func NewManage() *Manage { 41 | return &Manage{ 42 | NodeList: make([]*node.Node, 0), 43 | Subs: make([]*sub.Subscirbe, 0), 44 | Index: 0, 45 | Filter: make([]*node.NodeFilter, 0), 46 | } 47 | } 48 | 49 | // Save 保存数据 50 | func (m *Manage) Save() { 51 | err := core.WriteJSON(m, core.DataFile) 52 | if err != nil { 53 | log.Error(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/manage/recycle_manage.go: -------------------------------------------------------------------------------- 1 | package manage 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/node" 6 | "Txray/log" 7 | ) 8 | 9 | func (m *Manage) MoveToRecycle(n *node.Node) { 10 | if n != nil { 11 | m.RecycleNodeList = append(m.RecycleNodeList, n) 12 | } 13 | } 14 | 15 | func (m *Manage) MoveFormRecycle(key string) { 16 | indexList := core.IndexList(key, m.RecycleLen()) 17 | if len(indexList) == 0 { 18 | return 19 | } 20 | defer m.Save() 21 | newNodeList := make([]*node.Node, 0) 22 | m.RecycleForEach(func(i int, n *node.Node) { 23 | if HasIn(i, indexList) { 24 | m.addNode(n) 25 | log.Info("回收站 ==> ", n.GetName()) 26 | } else { 27 | newNodeList = append(newNodeList, n) 28 | } 29 | }) 30 | m.RecycleNodeList = newNodeList 31 | } 32 | 33 | func (m *Manage) RecycleLen() int { 34 | return len(m.RecycleNodeList) 35 | } 36 | 37 | func (m *Manage) getRecycleNode(i int) *node.Node { 38 | if i >= 0 && i < m.RecycleLen() { 39 | return m.RecycleNodeList[i] 40 | } 41 | return nil 42 | } 43 | 44 | func (m *Manage) GetRecycleNode(index int) *node.Node { 45 | return m.getRecycleNode(index - 1) 46 | } 47 | 48 | func (m *Manage) RecycleForEach(funC func(int, *node.Node)) { 49 | for i, n := range m.RecycleNodeList { 50 | funC(i+1, n) 51 | } 52 | } 53 | 54 | func (m *Manage) ClearRecycle() { 55 | m.RecycleNodeList = make([]*node.Node, 0) 56 | } 57 | -------------------------------------------------------------------------------- /core/sub/net.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // 通过http代理访问网站 12 | func GetByHTTPProxy(objUrl, proxyAddress string, proxyPort int, timeOut time.Duration) (*http.Response, error) { 13 | proxy := func(_ *http.Request) (*url.URL, error) { 14 | return url.Parse(fmt.Sprintf("http://%s:%d", proxyAddress, proxyPort)) 15 | } 16 | transport := &http.Transport{Proxy: proxy} 17 | client := &http.Client{ 18 | Transport: transport, 19 | Timeout: timeOut, 20 | } 21 | return client.Get(objUrl) 22 | } 23 | 24 | // 通过Socks5代理访问网站 25 | func GetBySocks5Proxy(objUrl, proxyAddress string, proxyPort int, timeOut time.Duration) (*http.Response, error) { 26 | 27 | proxy := func(_ *http.Request) (*url.URL, error) { 28 | return url.Parse(fmt.Sprintf("socks5://%s:%d", proxyAddress, proxyPort)) 29 | } 30 | transport := &http.Transport{Proxy: proxy} 31 | client := &http.Client{ 32 | Transport: transport, 33 | Timeout: timeOut, 34 | } 35 | return client.Get(objUrl) 36 | } 37 | 38 | // 不通过代理访问网站 39 | func GetNoProxy(url string, timeOut time.Duration) (*http.Response, error) { 40 | client := &http.Client{ 41 | Timeout: timeOut, 42 | } 43 | return client.Get(url) 44 | } 45 | 46 | // 读取http响应的内容 47 | func ReadDate(resp *http.Response) string { 48 | body, _ := io.ReadAll(resp.Body) 49 | return string(body) 50 | } 51 | -------------------------------------------------------------------------------- /core/setting/init.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/setting/key" 6 | "Txray/log" 7 | "os" 8 | 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | func init() { 13 | // 配置文件不存在则创建 14 | if _, err := os.Stat(core.SettingFile); os.IsNotExist(err) { 15 | file, _ := os.Create(core.SettingFile) 16 | _ = file.Close() 17 | } 18 | viper.SetConfigName("setting") 19 | viper.SetConfigType("toml") 20 | viper.AddConfigPath(core.GetConfigDir()) 21 | // 设置默认值 22 | viper.SetDefault(key.Socks, 23333) 23 | viper.SetDefault(key.Http, 0) 24 | viper.SetDefault(key.UDP, true) 25 | viper.SetDefault(key.Sniffing, true) 26 | viper.SetDefault(key.FromLanConn, false) 27 | viper.SetDefault(key.Mux, false) 28 | viper.SetDefault(key.AllowInsecure, false) 29 | 30 | viper.SetDefault(key.RoutingStrategy, "IPIfNonMatch") //路由策略 31 | viper.SetDefault(key.RoutingBypass, true) // 绕过局域网和大陆 32 | 33 | viper.SetDefault(key.DNSPort, 13500) 34 | viper.SetDefault(key.DNSForeign, "1.1.1.1") 35 | viper.SetDefault(key.DNSDomestic, "223.6.6.6") 36 | viper.SetDefault(key.DNSBackup, "114.114.114.114") 37 | 38 | viper.SetDefault(key.TestURL, "https://www.youtube.com") 39 | viper.SetDefault(key.TestTimeout, 5) 40 | viper.SetDefault(key.TestMinTime, 1000) 41 | viper.SetDefault(key.RunBefore, "") 42 | 43 | viper.SetDefault(key.PID, 0) 44 | // 读取配置文件 45 | err := viper.ReadInConfig() 46 | if err != nil { 47 | log.Error(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/protocols/protocols.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | ) 7 | 8 | type Protocol interface { 9 | // GetProtocolMode 获取协议模式 10 | GetProtocolMode() Mode 11 | // GetName 获取别名 12 | GetName() string 13 | // GetAddr 获取远程地址 14 | GetAddr() string 15 | // GetPort 获取远程端口 16 | GetPort() int 17 | // GetInfo 获取节点数据 18 | GetInfo() string 19 | // GetLink 获取节点分享链接 20 | GetLink() string 21 | } 22 | 23 | // 序列化 24 | func Serialize(p Protocol) string { 25 | jsonData, _ := json.Marshal(p) 26 | return string(p.GetProtocolMode()) + ": " + string(jsonData) 27 | } 28 | 29 | // 反序列化 30 | func Deserialize(text string) Protocol { 31 | expr := "(^[a-zA-Z]*?): ({.*?}$)" 32 | r, _ := regexp.Compile(expr) 33 | result := r.FindStringSubmatch(text) 34 | if len(result) == 3 { 35 | jsonText := result[2] 36 | var data Protocol 37 | switch result[1] { 38 | case string(ModeVMess): 39 | data = new(VMess) 40 | case string(ModeShadowSocks): 41 | data = new(ShadowSocks) 42 | case string(ModeShadowSocksR): 43 | data = new(ShadowSocksR) 44 | case string(ModeTrojan): 45 | data = new(Trojan) 46 | case string(ModeSocks): 47 | data = new(Socks) 48 | case string(ModeVLESS): 49 | data = new(VLess) 50 | case string(ModeVMessAEAD): 51 | data = new(VMessAEAD) 52 | } 53 | err := json.Unmarshal([]byte(jsonText), &data) 54 | if err != nil { 55 | return nil 56 | } 57 | return data 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module Txray 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/abiosoft/ishell v2.0.0+incompatible 7 | github.com/atotto/clipboard v0.1.4 8 | github.com/mattn/go-runewidth v0.0.13 9 | github.com/olekukonko/tablewriter v0.0.5 10 | github.com/spf13/viper v1.9.0 11 | go.uber.org/zap v1.19.1 12 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 13 | ) 14 | 15 | require ( 16 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect 17 | github.com/fatih/color v1.9.0 // indirect 18 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect 19 | github.com/fsnotify/fsnotify v1.5.1 // indirect 20 | github.com/hashicorp/hcl v1.0.0 // indirect 21 | github.com/hpcloud/tail v1.0.0 22 | github.com/magiconair/properties v1.8.5 // indirect 23 | github.com/mattn/go-colorable v0.1.6 // indirect 24 | github.com/mattn/go-isatty v0.0.12 // indirect 25 | github.com/mitchellh/mapstructure v1.4.2 // indirect 26 | github.com/pelletier/go-toml v1.9.4 // indirect 27 | github.com/rivo/uniseg v0.2.0 // indirect 28 | github.com/spf13/afero v1.6.0 // indirect 29 | github.com/spf13/cast v1.4.1 // indirect 30 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 31 | github.com/spf13/pflag v1.0.5 // indirect 32 | github.com/subosito/gotenv v1.2.0 // indirect 33 | go.uber.org/atomic v1.7.0 // indirect 34 | go.uber.org/multierr v1.6.0 // indirect 35 | golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect 36 | golang.org/x/text v0.3.6 // indirect 37 | gopkg.in/fsnotify.v1 v1.4.7 // indirect 38 | gopkg.in/ini.v1 v1.63.2 // indirect 39 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 40 | gopkg.in/yaml.v2 v2.4.0 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /core/protocols/trojan.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type Trojan struct { 10 | url.Values 11 | Password string `json:"password"` 12 | Address string `json:"address"` 13 | Port int `json:"port"` 14 | Remarks string `json:"remarks"` 15 | } 16 | 17 | func (t *Trojan) GetProtocolMode() Mode { 18 | return ModeTrojan 19 | } 20 | 21 | func (t *Trojan) GetName() string { 22 | return t.Remarks 23 | } 24 | func (t *Trojan) GetAddr() string { 25 | return t.Address 26 | } 27 | 28 | func (t *Trojan) GetPort() int { 29 | return t.Port 30 | } 31 | 32 | func (t *Trojan) GetInfo() string { 33 | var buf bytes.Buffer 34 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "别名", t.Remarks)) 35 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "地址", t.Address)) 36 | buf.WriteString(fmt.Sprintf("%3s: %d\n", "端口", t.Port)) 37 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "密码", t.Password)) 38 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "SNI", t.Sni())) 39 | buf.WriteString(fmt.Sprintf("%3s: %s", "协议", t.GetProtocolMode())) 40 | return buf.String() 41 | } 42 | 43 | func (t *Trojan) GetLink() string { 44 | u := &url.URL{ 45 | Scheme: "trojan", 46 | Host: fmt.Sprintf("%s:%d", t.Address, t.Port), 47 | Fragment: t.Remarks, 48 | User: url.User(t.Password), 49 | RawQuery: t.Values.Encode(), 50 | } 51 | return u.String() 52 | } 53 | 54 | func (t *Trojan) Sni() string { 55 | if t.Has("sni") { 56 | return t.Get("sni") 57 | } 58 | return "" 59 | } 60 | 61 | func (t *Trojan) Check() *Trojan { 62 | if t.Password != "" && t.Address != "" && t.Port > 0 && t.Port < 65535 && t.Remarks != "" { 63 | return t 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /cmd/shell.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/xray" 6 | 7 | "github.com/abiosoft/ishell" 8 | ) 9 | 10 | func InitShell(shell *ishell.Shell) { 11 | shell.AddCmd(&ishell.Cmd{ 12 | Name: "version", 13 | Aliases: []string{"-v", "--version"}, 14 | Help: "程序版本", 15 | Func: func(c *ishell.Context) { 16 | c.Printf("%s version \"%s\"\n", c.Get("name"), c.Get("version")) 17 | }, 18 | }) 19 | shell.AddCmd(&ishell.Cmd{ 20 | Name: "help", 21 | Aliases: []string{"-h", "--help"}, 22 | Help: "帮助信息", 23 | Func: func(c *ishell.Context) { 24 | c.Println(help.Help) 25 | }, 26 | }) 27 | InitSettingShell(shell) 28 | InitNodeShell(shell) 29 | InitSubscribeShell(shell) 30 | InitFilterShell(shell) 31 | InitRecycleShell(shell) 32 | InitRouteShell(shell) 33 | InitServiceShell(shell) 34 | InitAliasShell(shell) 35 | shell.AddCmd(&ishell.Cmd{ 36 | Name: "log", 37 | Func: func(c *ishell.Context) { 38 | xray.ShowLog() 39 | }, 40 | }) 41 | } 42 | 43 | // 参数解析 44 | func FlagsParse(args []string, keys map[string]string) map[string]string { 45 | resultMap := make(map[string]string) 46 | key := "data" 47 | for _, x := range args { 48 | if len(x) >= 2 { 49 | if x[:2] == "--" { 50 | key = x[2:] 51 | resultMap[key] = "" 52 | } else if x[:1] == "-" { 53 | if x[1] >= 48 && x[1] <= 57 { 54 | resultMap[key] = x 55 | } else if len(x) == 2 { 56 | d, ok := keys[x[1:]] 57 | if ok { 58 | key = d 59 | } else { 60 | key = x[1:] 61 | } 62 | resultMap[key] = "" 63 | } 64 | } else { 65 | resultMap[key] = x 66 | } 67 | } else if len(x) > 0 { 68 | resultMap[key] = x 69 | } 70 | } 71 | return resultMap 72 | } 73 | -------------------------------------------------------------------------------- /core/setting/base.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "Txray/core/setting/key" 5 | "errors" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func Socks() int { 10 | return viper.GetInt(key.Socks) 11 | } 12 | 13 | func SetSocks(port int) error { 14 | if port < 1 || port > 65535 { 15 | return errors.New("socks端口取值 1~65535") 16 | } 17 | viper.Set(key.Socks, port) 18 | return viper.WriteConfig() 19 | } 20 | 21 | func Http() int { 22 | return viper.GetInt(key.Http) 23 | } 24 | 25 | func SetHttp(port int) error { 26 | if port < 0 || port > 65535 { 27 | return errors.New("http端口取值 0~65535") 28 | } 29 | viper.Set(key.Http, port) 30 | return viper.WriteConfig() 31 | } 32 | 33 | func UDP() bool { 34 | return viper.GetBool(key.UDP) 35 | } 36 | 37 | func SetUDP(status bool) error { 38 | viper.Set(key.UDP, status) 39 | return viper.WriteConfig() 40 | } 41 | 42 | func Sniffing() bool { 43 | return viper.GetBool(key.Sniffing) 44 | } 45 | 46 | func SetSniffing(status bool) error { 47 | viper.Set(key.Sniffing, status) 48 | return viper.WriteConfig() 49 | } 50 | 51 | func FromLanConn() bool { 52 | return viper.GetBool(key.FromLanConn) 53 | } 54 | 55 | func SetFromLanConn(status bool) error { 56 | viper.Set(key.FromLanConn, status) 57 | return viper.WriteConfig() 58 | } 59 | 60 | func Mux() bool { 61 | return viper.GetBool(key.Mux) 62 | } 63 | 64 | func SetMux(status bool) error { 65 | viper.Set(key.Mux, status) 66 | return viper.WriteConfig() 67 | } 68 | 69 | func Pid() int { 70 | return viper.GetInt(key.PID) 71 | } 72 | 73 | func SetPid(pid int) error { 74 | viper.Set(key.PID, pid) 75 | return viper.WriteConfig() 76 | } 77 | 78 | func AllowInsecure() bool { 79 | return viper.GetBool(key.AllowInsecure) 80 | } 81 | 82 | func SetAllowInsecure(status bool) error { 83 | viper.Set(key.AllowInsecure, status) 84 | return viper.WriteConfig() 85 | } -------------------------------------------------------------------------------- /cmd/recycle.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/core" 6 | "Txray/core/manage" 7 | "github.com/abiosoft/ishell" 8 | "github.com/olekukonko/tablewriter" 9 | "os" 10 | "strconv" 11 | ) 12 | 13 | func InitRecycleShell(shell *ishell.Shell) { 14 | recycleCmd := &ishell.Cmd{ 15 | Name: "recycle", 16 | Func: func(c *ishell.Context) { 17 | var key string 18 | if len(c.Args) == 1 { 19 | key = c.Args[0] 20 | } else { 21 | key = "all" 22 | } 23 | table := tablewriter.NewWriter(os.Stdout) 24 | table.SetHeader([]string{"索引", "协议", "别名", "地址", "端口"}) 25 | table.SetAlignment(tablewriter.ALIGN_CENTER) 26 | center := tablewriter.ALIGN_CENTER 27 | left := tablewriter.ALIGN_LEFT 28 | table.SetColumnAlignment([]int{center, center, left, center, center, center}) 29 | table.SetColWidth(70) 30 | for _, index := range core.IndexList(key, manage.Manager.RecycleLen()) { 31 | n := manage.Manager.GetRecycleNode(index) 32 | if n != nil { 33 | table.Append([]string{ 34 | strconv.Itoa(index), 35 | string(n.GetProtocolMode()), 36 | n.GetName(), 37 | n.GetAddr(), 38 | strconv.Itoa(n.GetPort()), 39 | }) 40 | } 41 | } 42 | table.Render() 43 | }, 44 | } 45 | // help 46 | recycleCmd.AddCmd(&ishell.Cmd{ 47 | Name: "help", 48 | Aliases: []string{"-h", "--help"}, 49 | Func: func(c *ishell.Context) { 50 | c.Println(help.Recycle) 51 | }, 52 | }) 53 | // restore 54 | recycleCmd.AddCmd(&ishell.Cmd{ 55 | Name: "restore", 56 | Func: func(c *ishell.Context) { 57 | if len(c.Args) == 1 { 58 | manage.Manager.MoveFormRecycle(c.Args[0]) 59 | } 60 | }, 61 | }) 62 | // clear 63 | recycleCmd.AddCmd(&ishell.Cmd{ 64 | Name: "clear", 65 | Func: func(c *ishell.Context) { 66 | manage.Manager.ClearRecycle() 67 | }, 68 | }) 69 | shell.AddCmd(recycleCmd) 70 | } 71 | -------------------------------------------------------------------------------- /core/protocols/socks.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type Socks struct { 10 | Address string `json:"address"` 11 | Port int `json:"port"` 12 | Username string `json:"username"` 13 | Password string `json:"password"` 14 | Remarks string `json:"remarks"` 15 | } 16 | 17 | // GetProtocolMode 获取协议模式 18 | func (s *Socks) GetProtocolMode() Mode { 19 | return ModeSocks 20 | } 21 | 22 | // GetName 获取别名 23 | func (s *Socks) GetName() string { 24 | return s.Remarks 25 | } 26 | 27 | // GetAddr 获取远程地址 28 | func (s *Socks) GetAddr() string { 29 | return s.Address 30 | } 31 | 32 | // GetPort 获取远程端口 33 | func (s *Socks) GetPort() int { 34 | return s.Port 35 | } 36 | 37 | // GetInfo 获取节点数据 38 | func (s *Socks) GetInfo() string { 39 | var buf bytes.Buffer 40 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "别名", s.Remarks)) 41 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "地址", s.Address)) 42 | buf.WriteString(fmt.Sprintf("%7s: %d\n", "端口", s.Port)) 43 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "用户", s.Username)) 44 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "密码", s.Password)) 45 | buf.WriteString(fmt.Sprintf("%7s: %s", "协议", s.GetProtocolMode())) 46 | return buf.String() 47 | } 48 | 49 | // GetLink 获取节点分享链接 50 | func (s *Socks) GetLink() string { 51 | u := url.URL{ 52 | Scheme: "socks", 53 | Host: fmt.Sprintf("%s:%d", s.Address, s.Port), 54 | Fragment: s.Remarks, 55 | } 56 | if s.Username != "" && s.Password != "" { 57 | u.User = url.UserPassword(s.Username, s.Password) 58 | } 59 | return u.String() 60 | } 61 | 62 | func (s *Socks) Check() *Socks { 63 | if s.Address != "" && s.Port > 0 && s.Port < 65535 && s.Remarks != "" { 64 | if s.Username == "" && s.Password == "" { 65 | return s 66 | } 67 | if s.Username != "" && s.Password != "" { 68 | return s 69 | } 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /core/node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "Txray/core/protocols" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type Node struct { 13 | protocols.Protocol `json:"-"` 14 | SubID string `json:"sub_id"` 15 | Data string `json:"data"` 16 | TestResult float64 `json:"-"` 17 | } 18 | 19 | func (n *Node) TestResultStr() string { 20 | if n.TestResult == 0 { 21 | return "" 22 | } else if n.TestResult >= 99998 { 23 | return "-1ms" 24 | } else { 25 | return fmt.Sprintf("%.4vms", n.TestResult) 26 | } 27 | } 28 | 29 | // NewNode New一个节点 30 | func NewNode(link, id string) *Node { 31 | if data := protocols.ParseLink(link); data != nil { 32 | return &Node{Protocol: data, SubID: id} 33 | } 34 | return nil 35 | } 36 | 37 | func NewNodeByData(protocol protocols.Protocol) *Node { 38 | return &Node{Protocol: protocol} 39 | } 40 | 41 | // ParseData 反序列化Data 42 | func (n *Node) ParseData() { 43 | n.Protocol = protocols.ParseLink(n.Data) 44 | } 45 | 46 | // Serialize2Data 序列化数据-->Data 47 | func (n *Node) Serialize2Data() { 48 | n.Data = n.GetLink() 49 | } 50 | 51 | var WG sync.WaitGroup 52 | 53 | func (n *Node) Tcping() { 54 | count := 3 55 | var sum float64 56 | var timeout time.Duration = 3 * time.Second 57 | isTimeout := false 58 | for i := 0; i < count; i++ { 59 | start := time.Now() 60 | d := net.Dialer{Timeout: timeout} 61 | conn, err := d.Dial("tcp", fmt.Sprintf("%s:%d", n.GetAddr(), n.GetPort())) 62 | if err != nil { 63 | isTimeout = true 64 | break 65 | } 66 | conn.Close() 67 | elapsed := time.Since(start) 68 | sum += float64(elapsed.Nanoseconds()) / 1e6 69 | } 70 | if isTimeout { 71 | n.TestResult = 99999 72 | } else { 73 | n.TestResult = sum / float64(count) 74 | } 75 | WG.Done() 76 | } 77 | 78 | func (n *Node) Show() { 79 | ShowTopBottomSepLine('=', strings.Split(n.GetInfo(), "\n")...) 80 | } 81 | -------------------------------------------------------------------------------- /core/setting/alias.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "Txray/core" 5 | "github.com/spf13/viper" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type Alias struct { 11 | Name string 12 | Cmd string 13 | } 14 | 15 | func NewAlias(name, cmd string) *Alias { 16 | return &Alias{ 17 | Name: name, 18 | Cmd: cmd, 19 | } 20 | } 21 | 22 | func (a *Alias) GetCmd() [][]string { 23 | result := make([][]string, 0) 24 | for _, line := range strings.Split(a.Cmd, "|") { 25 | cmd := strings.Fields(line) 26 | if cmd != nil { 27 | result = append(result, cmd) 28 | } 29 | } 30 | return result 31 | } 32 | 33 | func AliasList() []*Alias { 34 | result := make([]*Alias, 0) 35 | for k, v := range viper.GetStringMapString("alias") { 36 | result = append(result, NewAlias(k, v)) 37 | } 38 | if len(result) <= 1 { 39 | return result 40 | } 41 | for i := 1; i < len(result); i++ { 42 | preIndex := i - 1 43 | current := result[i] 44 | for preIndex >= 0 && result[preIndex].Name > current.Name { 45 | result[preIndex+1] = result[preIndex] 46 | preIndex -= 1 47 | } 48 | result[preIndex+1] = current 49 | } 50 | return result 51 | } 52 | 53 | func AddAlias(key, cmd string) { 54 | if ok, _ := regexp.MatchString("^[^ ]*$", key); ok { 55 | v := viper.Get("alias") 56 | if v == nil { 57 | viper.SetDefault("alias."+key, cmd) 58 | } else { 59 | v.(map[string]interface{})[key] = cmd 60 | } 61 | viper.WriteConfig() 62 | } 63 | } 64 | 65 | func DelAlias(key string) []string { 66 | result := make([]string, 0) 67 | aliasList := AliasList() 68 | indexList := core.IndexList(key, len(aliasList)) 69 | if len(indexList) == 0 { 70 | delAlias(key) 71 | result = append(result, key) 72 | } else { 73 | for _, index := range indexList { 74 | delAlias(aliasList[index-1].Name) 75 | result = append(result, aliasList[index-1].Name) 76 | } 77 | } 78 | return result 79 | } 80 | 81 | func delAlias(key string) { 82 | delete(viper.Get("alias").(map[string]interface{}), key) 83 | viper.WriteConfig() 84 | } 85 | -------------------------------------------------------------------------------- /core/node/filter.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type NodeFilter struct { 10 | Mode FilterMode `json:"mode"` 11 | Re string `json:"re"` 12 | IsUse bool `json:"is_use"` 13 | } 14 | 15 | type FilterMode string 16 | 17 | const ( 18 | NameFilter FilterMode = "name" 19 | AddrFilter FilterMode = "addr" 20 | PortFilter FilterMode = "port" 21 | ProtocolFilter FilterMode = "proto" 22 | ) 23 | 24 | func NewNodeFilter(key string) *NodeFilter { 25 | if strings.HasPrefix(key, "name:") { 26 | return &NodeFilter{Mode: NameFilter, Re: key[5:], IsUse: true} 27 | } else if strings.HasPrefix(key, "addr:") { 28 | return &NodeFilter{Mode: AddrFilter, Re: key[5:], IsUse: true} 29 | } else if strings.HasPrefix(key, "port:") { 30 | return &NodeFilter{Mode: PortFilter, Re: key[5:], IsUse: true} 31 | } else if strings.HasPrefix(key, "proto:") { 32 | return &NodeFilter{Mode: ProtocolFilter, Re: key[6:], IsUse: true} 33 | } else { 34 | return &NodeFilter{Mode: NameFilter, Re: key, IsUse: true} 35 | } 36 | 37 | } 38 | 39 | func (nf *NodeFilter) IsMatch(n *Node) bool { 40 | regexp, _ := regexp.Compile(nf.Re) 41 | if n != nil { 42 | return regexp.MatchString(nf.getData(n)) 43 | } 44 | return false 45 | } 46 | 47 | func (nf *NodeFilter) String() string { 48 | switch nf.Mode { 49 | case AddrFilter: 50 | return "addr:" + nf.Re 51 | case PortFilter: 52 | return "port:" + nf.Re 53 | case ProtocolFilter: 54 | return "proto:" + nf.Re 55 | case NameFilter: 56 | return "name:" + nf.Re 57 | default: 58 | return "name:" + nf.Re 59 | } 60 | } 61 | 62 | func (nf *NodeFilter) getData(n *Node) string { 63 | if n == nil { 64 | return "" 65 | } 66 | switch nf.Mode { 67 | case AddrFilter: 68 | return n.GetAddr() 69 | case PortFilter: 70 | return strconv.Itoa(n.GetPort()) 71 | case ProtocolFilter: 72 | return string(n.GetProtocolMode()) 73 | case NameFilter: 74 | return n.GetName() 75 | default: 76 | return n.GetName() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /core/protocols/shadowsocksR.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | type ShadowSocksR struct { 9 | Address string `json:"address"` 10 | Port int `json:"port"` 11 | Protocol string `json:"protocol"` 12 | Method string `json:"method"` 13 | Obfs string `json:"obfs"` 14 | Password string `json:"password"` 15 | ObfsParam string `json:"obfsParam"` 16 | ProtoParam string `json:"protoParam"` 17 | Remarks string `json:"remarks"` 18 | Group string `json:"group"` 19 | } 20 | 21 | func (s *ShadowSocksR) GetProtocolMode() Mode { 22 | return ModeShadowSocksR 23 | } 24 | 25 | func (s *ShadowSocksR) GetName() string { 26 | return s.Remarks 27 | } 28 | func (s *ShadowSocksR) GetAddr() string { 29 | return s.Address 30 | } 31 | 32 | func (s *ShadowSocksR) GetPort() int { 33 | return s.Port 34 | } 35 | 36 | func (s *ShadowSocksR) GetInfo() string { 37 | var buf bytes.Buffer 38 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "别名", s.Remarks)) 39 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "地址", s.Address)) 40 | buf.WriteString(fmt.Sprintf("%7s: %d\n", "端口", s.Port)) 41 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "加密方法", s.Method)) 42 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "密码", s.Password)) 43 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "协议", s.Protocol)) 44 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "协议参数", s.ProtoParam)) 45 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "混淆", s.Obfs)) 46 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "混淆参数", s.ObfsParam)) 47 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "分组", s.Group)) 48 | buf.WriteString(fmt.Sprintf("%7s: %s", "协议", s.GetProtocolMode())) 49 | return buf.String() 50 | } 51 | 52 | func (s *ShadowSocksR) GetLink() string { 53 | formatParams := "/?obfsparam=%s&protoparam=%s&remarks=%s&group=%s" 54 | params := fmt.Sprintf(formatParams, base64Encode(s.ObfsParam), base64Encode(s.ProtoParam), 55 | base64Encode(s.Remarks), base64Encode(s.Group)) 56 | result := fmt.Sprintf("%s:%d:%s:%s:%s:%s%s", s.Address, s.Port, s.Protocol, s.Method, s.Obfs, 57 | base64Encode(s.Password), params) 58 | return "ssr://" + base64EncodeWithEq(result) 59 | } 60 | -------------------------------------------------------------------------------- /core/protocols/shadowsocks.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type ShadowSocks struct { 10 | url.Values 11 | Password string `json:"password"` 12 | Address string `json:"address"` 13 | Port int `json:"port"` 14 | Remarks string `json:"remarks"` 15 | Method string `json:"method"` 16 | } 17 | 18 | func (s *ShadowSocks) GetProtocolMode() Mode { 19 | return ModeShadowSocks 20 | } 21 | 22 | func (s *ShadowSocks) GetName() string { 23 | return s.Remarks 24 | } 25 | func (s *ShadowSocks) GetAddr() string { 26 | return s.Address 27 | } 28 | 29 | func (s *ShadowSocks) GetPort() int { 30 | return s.Port 31 | } 32 | 33 | func (s *ShadowSocks) GetInfo() string { 34 | var buf bytes.Buffer 35 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "别名", s.Remarks)) 36 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "地址", s.Address)) 37 | buf.WriteString(fmt.Sprintf("%3s: %d\n", "端口", s.Port)) 38 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "密码", s.Password)) 39 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "加密", s.Method)) 40 | if s.Has("plugin") { 41 | buf.WriteString(fmt.Sprintf("%3s: %s\n", "其他", "plugin="+s.Get("plugin"))) 42 | } 43 | buf.WriteString(fmt.Sprintf("%3s: %s", "协议", s.GetProtocolMode())) 44 | return buf.String() 45 | } 46 | 47 | func (s *ShadowSocks) GetLink() string { 48 | if len(s.Values) == 0 { 49 | src := fmt.Sprintf("%s:%s@%s:%d", s.Method, s.Password, s.Address, s.GetPort()) 50 | return fmt.Sprintf("ss://%s#%s", base64EncodeWithEq(src), url.QueryEscape(s.Remarks)) 51 | } else { 52 | src := fmt.Sprintf("%s:%s", s.Method, s.Password) 53 | u := &url.URL{ 54 | Scheme: "ss", 55 | Host: fmt.Sprintf("%s:%d", s.Address, s.Port), 56 | Fragment: s.Remarks, 57 | User: url.User(base64EncodeWithEq(src)), 58 | RawQuery: s.Values.Encode(), 59 | } 60 | return u.String() 61 | } 62 | } 63 | 64 | func (s *ShadowSocks) Check() *ShadowSocks { 65 | if s.Address != "" && s.Port > 0 && s.Port < 65535 && s.Remarks != "" { 66 | if s.Method == "none" { 67 | return s 68 | } else if s.Password != "" && s.Method != "" { 69 | return s 70 | } 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /core/key.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "regexp" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func Range(srart, end int) []int { 11 | result := make([]int, 0) 12 | if srart <= end { 13 | for srart <= end { 14 | result = append(result, srart) 15 | srart++ 16 | } 17 | } else { 18 | for srart >= end { 19 | result = append(result, srart) 20 | srart-- 21 | } 22 | } 23 | return result 24 | } 25 | 26 | func IndexList(key string, max int) []int { 27 | if max == 0 { 28 | return []int{} 29 | } 30 | if key == "all" { 31 | return Range(1, max) 32 | } 33 | result := make([]int, 0) 34 | for _, item := range strings.Split(key, ",") { 35 | item = strings.Trim(item, " ") 36 | re1 := "^[1-9][0-9]*$" 37 | re2 := "(^[0-9]*)-([0-9]*$)" 38 | if re, _ := regexp.Compile(re1); re.MatchString(item) { 39 | i, _ := strconv.Atoi(item) 40 | if i > 0 && i <= max { 41 | result = append(result, i) 42 | } 43 | continue 44 | } 45 | if re, _ := regexp.Compile(re2); re.MatchString(item) { 46 | start := 1 47 | end := max 48 | s := re.FindStringSubmatch(item) 49 | if s[1] != "" { 50 | start, _ = strconv.Atoi(s[1]) 51 | } 52 | if s[2] != "" { 53 | end, _ = strconv.Atoi(s[2]) 54 | } 55 | if start > end { 56 | start, end = end, start 57 | } 58 | if start > max || end < 1 { 59 | continue 60 | } 61 | if start < 1 { 62 | start = 1 63 | } 64 | if end > max { 65 | end = max 66 | } 67 | result = append(result, Range(start, end)...) 68 | continue 69 | } 70 | } 71 | result = RemoveRepByMap(result) 72 | sort.Ints(result) 73 | return result 74 | } 75 | 76 | // RemoveRepByMap 通过map主键唯一的特性过滤重复元素 77 | func RemoveRepByMap(slc []int) []int { 78 | result := []int{} 79 | tempMap := map[int]byte{} // 存放不重复主键 80 | for _, e := range slc { 81 | l := len(tempMap) 82 | tempMap[e] = 0 83 | if len(tempMap) != l { // 加入map后,map长度变化,则元素不重复 84 | result = append(result, e) 85 | } 86 | } 87 | return result 88 | } 89 | 90 | func Reverse(slc []int) []int { 91 | for i := 0; i < len(slc)/2; i++ { 92 | j := len(slc) - i - 1 93 | slc[i], slc[j] = slc[j], slc[i] 94 | } 95 | return slc 96 | } 97 | -------------------------------------------------------------------------------- /core/protocols/field/field.go: -------------------------------------------------------------------------------- 1 | package field 2 | 3 | type Field struct { 4 | Key string // 字段名 5 | Value string // 默认值 6 | } 7 | 8 | func NilStrField(key string) Field { 9 | return Field{ 10 | Key: key, 11 | Value: "", 12 | } 13 | } 14 | 15 | func NoneField(key string) Field { 16 | return Field{ 17 | Key: key, 18 | Value: "none", 19 | } 20 | } 21 | 22 | func NewField(key, value string) Field { 23 | return Field{ 24 | Key: key, 25 | Value: value, 26 | } 27 | } 28 | 29 | var ( 30 | NetworkType Field = NewField("type", "tcp") // 协议的传输方式, 可选值 tcp/kcp/ws/http/quic/grpc 31 | VLessEncryption = NoneField("encryption") // 加密, VLESS可选值 none 32 | VMessEncryption = NewField("encryption", "auto") // 加密, VMess可选值 auto/aes-128-gcm/chacha20-poly1305/none 33 | TlsSecurity = NoneField("security") // 设定底层传输所使用的 TLS 类型, 可选值有 none/tls/xtls/reality 34 | 35 | // TCP 36 | TCPHeaderType = NoneField("headerType") 37 | 38 | // HTTP/2 39 | H2Path = NewField("path", "/") 40 | H2Host = NilStrField("host") 41 | 42 | // WebSocket 43 | WsPath = NewField("path", "/") 44 | WsHost = NilStrField("host") 45 | 46 | // mKCP 47 | MkcpHeaderType = NoneField("headerType") // mKCP 的伪装头部类型, 可选值 none/srtp/utp/wechat-video/dtls/wireguard 48 | Seed = NilStrField("seed") // mKCP 种子 49 | 50 | // QUIC 51 | QuicSecurity = NoneField("quicSecurity") // QUIC 的加密方式, 可选值 none/aes-128-gcm/chacha20-poly1305 52 | QuicKey = NilStrField("key") // QUIC 的加密方式不为 none 时的加密密钥 53 | QuicHeaderType = NoneField("headerType") // QUIC 的伪装头部类型。其他同 mKCP headerType 字段定义 54 | 55 | // gRPC 56 | GrpcServiceName = NilStrField("serviceName") 57 | GrpcMode = NewField("mode", "gun") // gRPC 的传输模式, 可选值 gun/multi/guna 58 | 59 | Security = NoneField("security") 60 | SNI = NilStrField("sni") // TLS SNI 61 | Alpn = NilStrField("alpn") // alpn 多选 h2,http/1.1 62 | Flow = NilStrField("flow") // XTLS 的流控方式,可选值xtls-rprx-direct/xtls-rprx-splice 63 | 64 | FingerPrint = NewField("fp", "chrome") // TLS Client Hello 指纹 65 | PublicKey = NilStrField("pbk") // REALITY的公钥 66 | ShortId = NilStrField("sid") // REALITY 的 ID 67 | SpiderX = NilStrField("spx") // REALITY 的爬虫 68 | ) 69 | -------------------------------------------------------------------------------- /cmd/filter.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/core/manage" 6 | "Txray/core/node" 7 | "github.com/abiosoft/ishell" 8 | "github.com/olekukonko/tablewriter" 9 | "os" 10 | "strconv" 11 | ) 12 | 13 | func InitFilterShell(shell *ishell.Shell) { 14 | filterCmd := &ishell.Cmd{ 15 | Name: "filter", 16 | Func: func(c *ishell.Context) { 17 | table := tablewriter.NewWriter(os.Stdout) 18 | table.SetHeader([]string{"索引", "规则", "是否启用"}) 19 | table.SetAlignment(tablewriter.ALIGN_CENTER) 20 | manage.Manager.FilterForEach(func(i int, filter *node.NodeFilter) { 21 | table.Append([]string{ 22 | strconv.Itoa(i), 23 | filter.String(), 24 | strconv.FormatBool(filter.IsUse), 25 | }) 26 | }) 27 | table.Render() 28 | }, 29 | } 30 | // help 31 | filterCmd.AddCmd(&ishell.Cmd{ 32 | Name: "help", 33 | Aliases: []string{"-h", "--help"}, 34 | Func: func(c *ishell.Context) { 35 | c.Println(help.Filter) 36 | }, 37 | }) 38 | // add 39 | filterCmd.AddCmd(&ishell.Cmd{ 40 | Name: "add", 41 | Func: func(c *ishell.Context) { 42 | if len(c.Args) == 1 { 43 | manage.Manager.AddFilter(c.Args[0]) 44 | _: 45 | shell.Process("filter") 46 | } 47 | }, 48 | }) 49 | // run 50 | filterCmd.AddCmd(&ishell.Cmd{ 51 | Name: "run", 52 | Func: func(c *ishell.Context) { 53 | if len(c.Args) == 0 { 54 | manage.Manager.RunFilter("") 55 | } else { 56 | manage.Manager.RunFilter(c.Args[0]) 57 | } 58 | 59 | }, 60 | }) 61 | // rm 62 | filterCmd.AddCmd(&ishell.Cmd{ 63 | Name: "rm", 64 | Aliases: []string{"del"}, 65 | Func: func(c *ishell.Context) { 66 | if len(c.Args) == 1 { 67 | manage.Manager.DelFilter(c.Args[0]) 68 | } 69 | _ = shell.Process("filter") 70 | }, 71 | }) 72 | // open 73 | filterCmd.AddCmd(&ishell.Cmd{ 74 | Name: "open", 75 | Func: func(c *ishell.Context) { 76 | if len(c.Args) == 1 { 77 | manage.Manager.SetFilter(c.Args[0], true) 78 | } 79 | _ = shell.Process("filter") 80 | }, 81 | }) 82 | //close 83 | filterCmd.AddCmd(&ishell.Cmd{ 84 | Name: "close", 85 | Func: func(c *ishell.Context) { 86 | if len(c.Args) == 1 { 87 | manage.Manager.SetFilter(c.Args[0], false) 88 | } 89 | _ = shell.Process("filter") 90 | }, 91 | }) 92 | shell.AddCmd(filterCmd) 93 | } 94 | -------------------------------------------------------------------------------- /core/protocols/vmess.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | type VMess struct { 11 | V string `json:"v"` 12 | Ps string `json:"ps"` 13 | Add string `json:"add"` 14 | Port int `json:"port"` 15 | Id string `json:"id"` 16 | Scy string `json:"scy"` 17 | Aid int `json:"aid"` 18 | Net string `json:"net"` 19 | Type string `json:"type"` 20 | Host string `json:"host"` 21 | Path string `json:"path"` 22 | Tls string `json:"tls"` 23 | Sni string `json:"sni"` 24 | Alpn string `json:"alpn"` 25 | } 26 | 27 | func (v *VMess) GetProtocolMode() Mode { 28 | return ModeVMess 29 | } 30 | 31 | func (v *VMess) GetName() string { 32 | return v.Ps 33 | } 34 | 35 | func (v *VMess) GetAddr() string { 36 | return v.Add 37 | } 38 | 39 | func (v *VMess) GetPort() int { 40 | return v.Port 41 | } 42 | 43 | func (v *VMess) GetInfo() string { 44 | var buf bytes.Buffer 45 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "别名", v.Ps)) 46 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "地址", v.Add)) 47 | buf.WriteString(fmt.Sprintf("%7s: %d\n", "端口", v.Port)) 48 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "用户ID", v.Id)) 49 | buf.WriteString(fmt.Sprintf("%7s: %d\n", "额外ID", v.Aid)) 50 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "加密方式", v.Scy)) 51 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装类型", v.Type)) 52 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装域名", v.Host)) 53 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "传输协议", v.Net)) 54 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "path", v.Path)) 55 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "传输安全", v.Tls)) 56 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "配置版本", v.V)) 57 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "SNI", v.Sni)) 58 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Alpn", v.Alpn)) 59 | buf.WriteString(fmt.Sprintf("%7s: %s", "协议", v.GetProtocolMode())) 60 | return buf.String() 61 | } 62 | 63 | func (v *VMess) GetLink() string { 64 | data := map[string]string{ 65 | "v": v.V, 66 | "ps": v.Ps, 67 | "add": v.Add, 68 | "port": strconv.Itoa(v.Port), 69 | "id": v.Id, 70 | "aid": strconv.Itoa(v.Aid), 71 | "scy": v.Scy, 72 | "net": v.Net, 73 | "type": v.Type, 74 | "host": v.Host, 75 | "path": v.Path, 76 | "tls": v.Tls, 77 | "sni": v.Sni, 78 | "alpn": v.Alpn, 79 | } 80 | jsonData, _ := json.Marshal(data) 81 | return "vmess://" + base64EncodeWithEq(string(jsonData)) 82 | } 83 | 84 | func (v *VMess) Check() *VMess { 85 | if v.Add != "" && v.Port > 0 && v.Port <= 65535 && v.Ps != "" && v.Id != "" && v.Net != "" { 86 | return v 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /cmd/alias.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/core/setting" 6 | "Txray/log" 7 | "github.com/abiosoft/ishell" 8 | "github.com/olekukonko/tablewriter" 9 | "os" 10 | "strconv" 11 | ) 12 | 13 | var names = make(map[string]int) 14 | 15 | func InitAliasShell(shell *ishell.Shell) { 16 | // 读取内置命令 17 | for _, cmd := range shell.Cmds() { 18 | names[cmd.Name] = 0 19 | } 20 | LoadAlias(shell) 21 | aliasCmd := &ishell.Cmd{ 22 | Name: "alias", 23 | Func: func(c *ishell.Context) { 24 | table := tablewriter.NewWriter(os.Stdout) 25 | table.SetHeader([]string{"索引", "别名", "命令"}) 26 | table.SetAlignment(tablewriter.ALIGN_CENTER) 27 | table.SetColWidth(70) 28 | center := tablewriter.ALIGN_CENTER 29 | left := tablewriter.ALIGN_LEFT 30 | table.SetColumnAlignment([]int{center, center, left}) 31 | for i, alias := range setting.AliasList() { 32 | table.Append([]string{ 33 | strconv.Itoa(i + 1), 34 | alias.Name, 35 | alias.Cmd, 36 | }) 37 | } 38 | table.Render() 39 | }, 40 | } 41 | aliasCmd.AddCmd(&ishell.Cmd{ 42 | Name: "help", 43 | Aliases: []string{"-h", "--help"}, 44 | Func: func(c *ishell.Context) { 45 | c.Println(help.Alias) 46 | }, 47 | }) 48 | aliasCmd.AddCmd(&ishell.Cmd{ 49 | Name: "set", 50 | Aliases: []string{"add"}, 51 | Func: func(c *ishell.Context) { 52 | if len(c.Args) == 2 { 53 | if _, ok := names[c.Args[0]]; !ok { 54 | setting.AddAlias(c.Args[0], c.Args[1]) 55 | LoadAlias(shell) 56 | _ = shell.Process("alias") 57 | } else { 58 | log.Warnf("'%s' 是内置命令,不能被覆盖", c.Args[0]) 59 | } 60 | } 61 | }, 62 | }) 63 | aliasCmd.AddCmd(&ishell.Cmd{ 64 | Name: "rm", 65 | Func: func(c *ishell.Context) { 66 | if len(c.Args) == 1 { 67 | for _, alias := range setting.DelAlias(c.Args[0]) { 68 | shell.DeleteCmd(alias) 69 | } 70 | _ = shell.Process("alias") 71 | } 72 | }, 73 | }) 74 | shell.AddCmd(aliasCmd) 75 | } 76 | 77 | func LoadAlias(shell *ishell.Shell) { 78 | for _, a := range setting.AliasList() { 79 | AddAliasShell(shell, a) 80 | } 81 | } 82 | 83 | func AddAliasShell(shell *ishell.Shell, a *setting.Alias) { 84 | if a != nil { 85 | // 防止覆盖内置命令 86 | if _, ok := names[a.Name]; !ok { 87 | shell.AddCmd(&ishell.Cmd{ 88 | Name: a.Name, 89 | Func: func(c *ishell.Context) { 90 | for i, line := range a.GetCmd() { 91 | if i+1 == len(a.GetCmd()) { 92 | line = append(line, c.Args...) 93 | } 94 | shell.Process(line...) 95 | } 96 | }, 97 | }) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core/manage/filter_manage.go: -------------------------------------------------------------------------------- 1 | package manage 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/node" 6 | "Txray/log" 7 | ) 8 | 9 | func (m *Manage) AddFilter(key string) { 10 | m.Filter = append(m.Filter, node.NewNodeFilter(key)) 11 | m.Save() 12 | } 13 | 14 | func (m *Manage) RunFilter(key string) { 15 | defer m.Save() 16 | selectedNode := m.SelectedNode() 17 | newNodeList := make([]*node.Node, 0) 18 | if key == "" { 19 | m.NodeForEach(func(i int, n *node.Node) { 20 | if f := m.IsCanFilter(n); f != nil { 21 | log.Infof("规则 [%s] 过滤节点==> %s", f.String(), n.GetName()) 22 | } else { 23 | newNodeList = append(newNodeList, n) 24 | } 25 | }) 26 | 27 | } else if f := node.NewNodeFilter(key); f != nil { 28 | m.NodeForEach(func(i int, n *node.Node) { 29 | if f.IsMatch(n) { 30 | log.Infof("规则 [%s] 过滤节点==> %s", f.String(), n.GetName()) 31 | } else { 32 | newNodeList = append(newNodeList, n) 33 | } 34 | }) 35 | } 36 | m.NodeList = newNodeList 37 | m.SetSelectedIndexByNode(selectedNode) 38 | } 39 | 40 | func (m *Manage) FilterForEach(funC func(int, *node.NodeFilter)) { 41 | for i, f := range m.Filter { 42 | funC(i+1, f) 43 | } 44 | } 45 | 46 | func (m *Manage) getFilter(i int) *node.NodeFilter { 47 | if i >= 0 && i < m.FilterLen() { 48 | return m.Filter[i] 49 | } 50 | return nil 51 | } 52 | 53 | func (m *Manage) GetFilter(index int) *node.NodeFilter { 54 | return m.getFilter(index - 1) 55 | } 56 | 57 | func (m *Manage) DelFilter(key string) { 58 | indexList := core.IndexList(key, m.FilterLen()) 59 | if len(indexList) == 0 { 60 | return 61 | } 62 | defer m.Save() 63 | newFilterList := make([]*node.NodeFilter, 0) 64 | m.FilterForEach(func(i int, filter *node.NodeFilter) { 65 | if !HasIn(i, indexList) { 66 | newFilterList = append(newFilterList, filter) 67 | } 68 | }) 69 | m.Filter = newFilterList 70 | } 71 | 72 | func (m *Manage) SetFilter(key string, isOpen bool) { 73 | indexList := core.IndexList(key, m.FilterLen()) 74 | if len(indexList) == 0 { 75 | return 76 | } 77 | defer m.Save() 78 | for _, index := range indexList { 79 | if filter := m.GetFilter(index); filter != nil { 80 | filter.IsUse = isOpen 81 | } 82 | } 83 | } 84 | 85 | func (m *Manage) FilterLen() int { 86 | return len(m.Filter) 87 | } 88 | 89 | func (m *Manage) IsCanFilter(n *node.Node) *node.NodeFilter { 90 | if n == nil { 91 | return nil 92 | } 93 | var f *node.NodeFilter = nil 94 | m.FilterForEach(func(i int, filter *node.NodeFilter) { 95 | if filter.IsUse && filter.IsMatch(n) { 96 | f = filter 97 | } 98 | }) 99 | return f 100 | } 101 | -------------------------------------------------------------------------------- /core/routing/route.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/manage" 6 | "Txray/log" 7 | "strconv" 8 | ) 9 | 10 | // 添加规则 11 | func AddRule(rt Type, list ...string) int { 12 | defer route.save() 13 | count := 0 14 | for _, rule := range list { 15 | if rule != "" { 16 | r := &routing{ 17 | Data: rule, 18 | Mode: GetRuleMode(rule), 19 | } 20 | count += 1 21 | switch rt { 22 | case TypeBlock: 23 | route.Block = append(route.Block, r) 24 | case TypeDirect: 25 | route.Direct = append(route.Direct, r) 26 | case TypeProxy: 27 | route.Proxy = append(route.Proxy, r) 28 | } 29 | } 30 | } 31 | return count 32 | } 33 | 34 | func GetRule(rt Type, key string) [][]string { 35 | var rules []*routing 36 | switch rt { 37 | case TypeDirect: 38 | rules = route.Direct 39 | case TypeProxy: 40 | rules = route.Proxy 41 | case TypeBlock: 42 | rules = route.Block 43 | } 44 | indexList := core.IndexList(key, len(rules)) 45 | result := make([][]string, 0, len(indexList)) 46 | for _, x := range indexList { 47 | r := rules[x-1] 48 | result = append(result, []string{ 49 | strconv.Itoa(x), 50 | string(r.Mode), 51 | r.Data, 52 | }) 53 | } 54 | return result 55 | } 56 | 57 | // 对路由数据进行分组 58 | func GetRulesGroupData(rt Type) ([]string, []string) { 59 | ips := make([]string, 0) 60 | domains := make([]string, 0) 61 | var rules []*routing 62 | switch rt { 63 | case TypeDirect: 64 | rules = route.Direct 65 | case TypeProxy: 66 | rules = route.Proxy 67 | case TypeBlock: 68 | rules = route.Block 69 | } 70 | for _, x := range rules { 71 | if x.Mode == "Domain" { 72 | domains = append(domains, x.Data) 73 | } else { 74 | ips = append(ips, x.Data) 75 | } 76 | } 77 | return ips, domains 78 | } 79 | 80 | // 删除规则 81 | func DelRule(rt Type, key string) { 82 | var rules []*routing 83 | switch rt { 84 | case TypeDirect: 85 | rules = route.Direct 86 | case TypeProxy: 87 | rules = route.Proxy 88 | case TypeBlock: 89 | rules = route.Block 90 | } 91 | indexList := core.IndexList(key, len(rules)) 92 | if len(indexList) == 0 { 93 | return 94 | } 95 | defer route.save() 96 | result := make([]*routing, 0) 97 | for i, rule := range rules { 98 | if !manage.HasIn(i+1, indexList) { 99 | result = append(result, rule) 100 | } 101 | } 102 | switch rt { 103 | case TypeDirect: 104 | route.Direct = result 105 | case TypeProxy: 106 | route.Proxy = result 107 | case TypeBlock: 108 | route.Block = result 109 | } 110 | log.Info("删除了 [", len(indexList), "] 条规则") 111 | } 112 | 113 | func RuleLen(rt Type) int { 114 | switch rt { 115 | case TypeDirect: 116 | return len(route.Direct) 117 | case TypeProxy: 118 | return len(route.Proxy) 119 | case TypeBlock: 120 | return len(route.Block) 121 | default: 122 | return 0 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /core/manage/sub_manage.go: -------------------------------------------------------------------------------- 1 | package manage 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/node" 6 | "Txray/core/sub" 7 | "Txray/log" 8 | "strings" 9 | ) 10 | 11 | func (m *Manage) SubForEach(funC func(int, *sub.Subscirbe)) { 12 | for i, n := range m.Subs { 13 | funC(i+1, n) 14 | } 15 | } 16 | 17 | func (m *Manage) AddSubscirbe(subscirbe *sub.Subscirbe) { 18 | if m.HasSub(subscirbe.ID()) { 19 | log.Warn("该订阅链接已存在") 20 | } else { 21 | m.Subs = append(m.Subs, subscirbe) 22 | m.Save() 23 | 24 | } 25 | } 26 | 27 | func (m *Manage) SubLen() int { 28 | return len(m.Subs) 29 | } 30 | 31 | func (m *Manage) getSub(i int) *sub.Subscirbe { 32 | if i >= 0 && i < m.SubLen() { 33 | return m.Subs[i] 34 | } 35 | return nil 36 | } 37 | 38 | func (m *Manage) GetSub(i int) *sub.Subscirbe { 39 | return m.getSub(i - 1) 40 | } 41 | 42 | func (m *Manage) UpdataNode(opt sub.UpdataOption) { 43 | if opt.Key == "" { 44 | m.SubForEach(func(i int, subscirbe *sub.Subscirbe) { 45 | if subscirbe.Using { 46 | m.updataNode(subscirbe, opt) 47 | } 48 | }) 49 | } else { 50 | for _, index := range core.IndexList(opt.Key, m.SubLen()) { 51 | m.updataNode(m.GetSub(index), opt) 52 | } 53 | } 54 | } 55 | 56 | func (m *Manage) updataNode(subscirbe *sub.Subscirbe, opt sub.UpdataOption) { 57 | links := subscirbe.UpdataNode(opt) 58 | if len(links) == 0 { 59 | return 60 | } 61 | count := 0 62 | m.DelNodeById(subscirbe.ID()) 63 | for _, link := range links { 64 | if ok := m.AddNode(node.NewNode(link, subscirbe.ID())); ok { 65 | count += 1 66 | } 67 | } 68 | log.Infof("从订阅 [%s] 获取了 '%d' 个节点", subscirbe.Url, count) 69 | } 70 | 71 | func (m *Manage) HasSub(id string) bool { 72 | ok := false 73 | m.SubForEach(func(i int, subscirbe *sub.Subscirbe) { 74 | if subscirbe.ID() == id { 75 | ok = true 76 | } 77 | }) 78 | return ok 79 | } 80 | 81 | func (m *Manage) DelSub(key string) { 82 | indexList := core.IndexList(key, m.SubLen()) 83 | if len(indexList) == 0 { 84 | return 85 | } 86 | defer m.Save() 87 | newSubList := make([]*sub.Subscirbe, 0) 88 | m.SubForEach(func(i int, subscirbe *sub.Subscirbe) { 89 | if !HasIn(i, indexList) { 90 | newSubList = append(newSubList, subscirbe) 91 | } 92 | }) 93 | m.Subs = newSubList 94 | } 95 | 96 | func (m *Manage) SetSub(key string, using, url, name string) { 97 | indexList := core.IndexList(key, m.SubLen()) 98 | if len(indexList) == 0 { 99 | return 100 | } 101 | if len(indexList) != 1 && url != "" { 102 | log.Warn("订阅链接不可以批量更改") 103 | return 104 | } 105 | defer m.Save() 106 | for _, index := range indexList { 107 | subscribe := m.GetSub(index) 108 | switch strings.ToLower(using) { 109 | case "true", "yes", "y": 110 | subscribe.Using = true 111 | case "false", "no", "n": 112 | subscribe.Using = false 113 | } 114 | if url != "" { 115 | subscribe.Url = url 116 | } 117 | if name != "" { 118 | subscribe.Name = name 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | "gopkg.in/natefinch/lumberjack.v2" 7 | "os" 8 | "time" 9 | ) 10 | 11 | var logger *zap.SugaredLogger 12 | 13 | const ( 14 | DEBUG = zapcore.DebugLevel 15 | INFO = zapcore.InfoLevel 16 | WARN = zapcore.WarnLevel 17 | ERROR = zapcore.ErrorLevel 18 | DPANIC = zapcore.DPanicLevel 19 | PANIC = zapcore.PanicLevel 20 | FATAL = zapcore.FatalLevel 21 | ) 22 | 23 | func init() { 24 | Init(GetConsoleZapcore(INFO)) 25 | } 26 | 27 | // 获取控制台日志核心 28 | // 设置日志等级 29 | func GetConsoleZapcore(level zapcore.Level) zapcore.Core { 30 | encoder := zap.NewProductionEncoderConfig() 31 | encoder.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 32 | enc.AppendString(t.Format("2006-01-02 15:04:05")) 33 | } 34 | encoder.EncodeLevel = func(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { 35 | enc.AppendString("[" + level.String() + "]") 36 | } 37 | encoder.EncodeCaller = nil 38 | encoder.ConsoleSeparator = " " 39 | return zapcore.NewCore(zapcore.NewConsoleEncoder(encoder), os.Stdout, zap.NewAtomicLevelAt(level)) 40 | } 41 | 42 | // 获取文件日志核心 43 | // 设置绝对路径 44 | // 设置日志等级 45 | // 设置日志文件最大尺寸(单位:M) 46 | func GetFileZapcore(absPath string, level zapcore.Level, fileMaxSize int) zapcore.Core { 47 | syncWriter := zapcore.AddSync(&lumberjack.Logger{ 48 | Filename: absPath, 49 | MaxSize: (1 << 20) * fileMaxSize, //20M 50 | LocalTime: true, 51 | Compress: true, 52 | }) 53 | encoder := zap.NewProductionEncoderConfig() 54 | encoder.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 55 | enc.AppendString(t.Format("2006-01-02 15:04:05")) 56 | } 57 | return zapcore.NewCore(zapcore.NewJSONEncoder(encoder), syncWriter, zap.NewAtomicLevelAt(level)) 58 | } 59 | 60 | func Init(cores ...zapcore.Core) { 61 | core := zapcore.NewTee(cores...) 62 | zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) 63 | logger = zapLogger.Sugar() 64 | } 65 | 66 | func Debug(args ...interface{}) { 67 | logger.Debug(args...) 68 | } 69 | 70 | func Debugf(template string, args ...interface{}) { 71 | logger.Debugf(template, args...) 72 | } 73 | 74 | func Info(args ...interface{}) { 75 | logger.Info(args...) 76 | } 77 | 78 | func Infof(template string, args ...interface{}) { 79 | logger.Infof(template, args...) 80 | } 81 | 82 | func Warn(args ...interface{}) { 83 | logger.Warn(args...) 84 | } 85 | 86 | func Warnf(template string, args ...interface{}) { 87 | logger.Warnf(template, args...) 88 | } 89 | 90 | func Error(args ...interface{}) { 91 | logger.Error(args...) 92 | } 93 | 94 | func Errorf(template string, args ...interface{}) { 95 | logger.Errorf(template, args...) 96 | } 97 | 98 | func DPanic(args ...interface{}) { 99 | logger.DPanic(args...) 100 | } 101 | 102 | func DPanicf(template string, args ...interface{}) { 103 | logger.DPanicf(template, args...) 104 | } 105 | 106 | func Panic(args ...interface{}) { 107 | logger.Panic(args...) 108 | } 109 | 110 | func Panicf(template string, args ...interface{}) { 111 | logger.Panicf(template, args...) 112 | } 113 | 114 | func Fatal(args ...interface{}) { 115 | logger.Fatal(args...) 116 | } 117 | 118 | func Fatalf(template string, args ...interface{}) { 119 | logger.Fatalf(template, args...) 120 | } 121 | -------------------------------------------------------------------------------- /xray/check.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "Txray/log" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | const CoreName string = "xray" 13 | 14 | var XrayPath = "" 15 | 16 | func init() { 17 | checkXrayFile() 18 | checkResource() 19 | } 20 | 21 | // 检查xray程序 22 | func checkXrayFile() { 23 | // 1.检查环境变量CORE_HOME目录下 24 | xrayPath := os.Getenv("CORE_HOME") 25 | if xrayPath != "" { 26 | if IsExistExe(xrayPath, CoreName) { 27 | XrayPath = filepath.Join(xrayPath, CoreName) 28 | return 29 | } 30 | } 31 | // 2.检查当前可执行文件目录下(递归检查) 32 | path, _ := os.Executable() 33 | files, _ := FindFileByName(filepath.Dir(path), "xray", ".exe") 34 | if len(files) != 0 { 35 | XrayPath = files[0] 36 | return 37 | } 38 | // 3.检查PATH环境变量 39 | if temp := getExePath(CoreName); temp != "" { 40 | XrayPath = temp 41 | return 42 | } 43 | // 提示信息 44 | log.Error("在 ", filepath.Dir(path), " 下没有找到xray程序") 45 | log.Error("请在 https://github.com/XTLS/Xray-core/releases 下载最新版本") 46 | log.Error("并将解压后的文件夹或所有文件移动到 ", filepath.Dir(path), " 下") 47 | os.Exit(0) 48 | } 49 | 50 | // 检查xray程序的资源文件 51 | func checkResource() { 52 | var baseDir []string = make([]string, 0) 53 | baseDir = append(baseDir, os.Getenv("XRAY_LOCATION_ASSET")) 54 | baseDir = append(baseDir, os.Getenv("xray.location.asset")) 55 | baseDir = append(baseDir, filepath.Dir(XrayPath)) 56 | for _, dir := range baseDir { 57 | if dir != "" { 58 | if IsExistFile(filepath.Join(dir, "geoip.dat")) && IsExistFile(filepath.Join(dir, "geosite.dat")) { 59 | return 60 | } 61 | } 62 | } 63 | log.Error(fmt.Sprintf("在 %s 目录下没有找到资源文件 geoip.dat 和 geosite.dat", filepath.Dir(XrayPath))) 64 | log.Error("或者配置资源文件的环境变量 XRAY_LOCATION_ASSET") 65 | os.Exit(0) 66 | } 67 | 68 | func IsExistFile(file string) bool { 69 | fp, err := os.Stat(file) 70 | return err == nil && !fp.IsDir() 71 | } 72 | 73 | // 检查dirPath目录下是否存在filename程序 74 | func IsExistExe(dirPath, filename string) bool { 75 | if runtime.GOOS == "windows" { 76 | fp, err := os.Stat(filepath.Join(dirPath, filename+".exe")) 77 | if err == nil && !fp.IsDir() { 78 | return true 79 | } 80 | } 81 | fp, err := os.Stat(filepath.Join(dirPath, filename)) 82 | return err == nil && !fp.IsDir() 83 | } 84 | 85 | // 遍历目录,查找文件 86 | func FindFileByName(root, name, ext string) ([]string, error) { 87 | root = strings.TrimRight(root, string(os.PathSeparator)) 88 | paths, err := os.ReadDir(root) 89 | if err != nil { 90 | return nil, err 91 | } 92 | objList := make([]string, 0) 93 | for _, p := range paths { 94 | absPath := root + string(os.PathSeparator) + p.Name() 95 | if p.IsDir() { 96 | o, err := FindFileByName(absPath, name, ext) 97 | if err != nil { 98 | return nil, err 99 | } 100 | objList = append(objList, o...) 101 | } else { 102 | if p.Name() == name || p.Name() == name+ext { 103 | objList = append(objList, absPath) 104 | } 105 | } 106 | } 107 | return objList, nil 108 | } 109 | 110 | // 查找Path中可执行文件的路径 111 | func getExePath(name string) string { 112 | data := os.Getenv("PATH") 113 | sep := ":" 114 | if runtime.GOOS == "windows" { 115 | sep = ";" 116 | } 117 | for _, x := range strings.Split(data, sep) { 118 | if strings.TrimSpace(x) != "" { 119 | if IsExistExe(strings.TrimSpace(x), name) { 120 | return filepath.Join(strings.TrimSpace(x), name) 121 | } 122 | } 123 | } 124 | return "" 125 | } 126 | -------------------------------------------------------------------------------- /cmd/subscribe.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/core/manage" 6 | "Txray/core/sub" 7 | "Txray/log" 8 | "github.com/abiosoft/ishell" 9 | "github.com/olekukonko/tablewriter" 10 | "os" 11 | "strconv" 12 | ) 13 | 14 | func InitSubscribeShell(shell *ishell.Shell) { 15 | subCmd := &ishell.Cmd{ 16 | Name: "sub", 17 | Func: func(c *ishell.Context) { 18 | table := tablewriter.NewWriter(os.Stdout) 19 | table.SetHeader([]string{"索引", "别名", "订阅地址", "是否启用"}) 20 | table.SetAlignment(tablewriter.ALIGN_CENTER) 21 | manage.Manager.SubForEach(func(i int, subscirbe *sub.Subscirbe) { 22 | table.Append([]string{ 23 | strconv.Itoa(i), 24 | subscirbe.Name, 25 | subscirbe.Url, 26 | strconv.FormatBool(subscirbe.Using), 27 | }) 28 | }) 29 | table.Render() 30 | }, 31 | } 32 | // help 33 | subCmd.AddCmd(&ishell.Cmd{ 34 | Name: "help", 35 | Aliases: []string{"-h", "--help"}, 36 | Func: func(c *ishell.Context) { 37 | c.Println(help.Sub) 38 | }, 39 | }) 40 | // add 41 | subCmd.AddCmd(&ishell.Cmd{ 42 | Name: "add", 43 | Func: func(c *ishell.Context) { 44 | argMap := FlagsParse(c.Args, map[string]string{ 45 | "r": "remarks", 46 | }) 47 | if len(c.Args) >= 1 { 48 | if sublink, ok := argMap["data"]; ok { 49 | if remarksArg, ok := argMap["remarks"]; ok { 50 | manage.Manager.AddSubscirbe(sub.NewSubscirbe(sublink, remarksArg)) 51 | } else { 52 | manage.Manager.AddSubscirbe(sub.NewSubscirbe(sublink, "remarks")) 53 | } 54 | _ = shell.Process("sub") 55 | } else { 56 | log.Warn("需要输入一个订阅链接") 57 | } 58 | } else if len(c.Args) == 0 { 59 | log.Warn("还需要输入一个订阅链接") 60 | } 61 | }, 62 | }) 63 | // update-node 64 | subCmd.AddCmd(&ishell.Cmd{ 65 | Name: "update-node", 66 | Func: func(c *ishell.Context) { 67 | argMap := FlagsParse(c.Args, map[string]string{ 68 | "s": "socks", 69 | "h": "http", 70 | "a": "addr", 71 | }) 72 | opt := sub.UpdataOption{} 73 | opt.Key = argMap["data"] 74 | if socks, ok := argMap["socks"]; ok { 75 | if v, err := strconv.Atoi(socks); err == nil { 76 | if 0 < v && v <= 65535 { 77 | opt.Port = v 78 | } 79 | } 80 | opt.ProxyMode = sub.SOCKS 81 | } else if http, ok := argMap["http"]; ok { 82 | if v, err := strconv.Atoi(http); err == nil { 83 | if 0 < v && v <= 65535 { 84 | opt.Port = v 85 | } 86 | } 87 | opt.ProxyMode = sub.HTTP 88 | } 89 | if address, ok := argMap["addr"]; ok { 90 | opt.Addr = address 91 | } 92 | manage.Manager.UpdataNode(opt) 93 | }, 94 | }) 95 | // rm 96 | subCmd.AddCmd(&ishell.Cmd{ 97 | Name: "rm", 98 | Aliases: []string{"del"}, 99 | Func: func(c *ishell.Context) { 100 | if len(c.Args) == 1 { 101 | manage.Manager.DelSub(c.Args[0]) 102 | _ = shell.Process("sub") 103 | } 104 | }, 105 | }) 106 | // mv 107 | subCmd.AddCmd(&ishell.Cmd{ 108 | Name: "mv", 109 | Aliases: []string{"set"}, 110 | Func: func(c *ishell.Context) { 111 | argMap := FlagsParse(c.Args, map[string]string{ 112 | "r": "remarks", 113 | "u": "url", 114 | }) 115 | if key, ok := argMap["data"]; ok { 116 | 117 | url := argMap["url"] 118 | remarks := argMap["remarks"] 119 | 120 | using := "" 121 | if value, ok := argMap["using"]; ok { 122 | using = value 123 | } 124 | manage.Manager.SetSub(key, using, url, remarks) 125 | _ = shell.Process("sub") 126 | } 127 | }, 128 | }) 129 | shell.AddCmd(subCmd) 130 | } 131 | -------------------------------------------------------------------------------- /core/manage/node_manage.go: -------------------------------------------------------------------------------- 1 | package manage 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/node" 6 | "Txray/log" 7 | ) 8 | 9 | // NodeLen 节点数量 10 | func (m *Manage) NodeLen() int { 11 | return len(m.NodeList) 12 | } 13 | 14 | // GetNodeByIndex 获取节点 15 | func (m *Manage) GetNode(index int) *node.Node { 16 | return m.getNode(index - 1) 17 | } 18 | 19 | // getNode 获取节点 20 | func (m *Manage) getNode(i int) *node.Node { 21 | if i < m.NodeLen() && i >= 0 { 22 | return m.NodeList[i] 23 | } 24 | return nil 25 | } 26 | 27 | func (m *Manage) addNode(n *node.Node) bool { 28 | if n == nil { 29 | return false 30 | } 31 | if f := m.IsCanFilter(n); f != nil { 32 | log.Infof("规则 [%s] 过滤节点==> %s", f.String(), n.GetName()) 33 | return false 34 | } 35 | n.Serialize2Data() 36 | m.NodeList = append(m.NodeList, n) 37 | return true 38 | } 39 | 40 | func (m *Manage) AddNode(n *node.Node) bool { 41 | ok := false 42 | if ok = m.addNode(n); ok { 43 | m.Save() 44 | } 45 | return ok 46 | } 47 | 48 | func (m *Manage) NodeForEach(funC func(int, *node.Node)) { 49 | for i, n := range m.NodeList { 50 | funC(i+1, n) 51 | } 52 | } 53 | 54 | func (m *Manage) Tcping() { 55 | m.NodeForEach(func(i int, n *node.Node) { 56 | node.WG.Add(1) 57 | go n.Tcping() 58 | }) 59 | node.WG.Wait() 60 | defer m.Save() 61 | m.NodeSort(func(n1 *node.Node, n2 *node.Node) bool { 62 | return n1.TestResult < n2.TestResult 63 | }) 64 | m.SetSelectedIndex(1) 65 | 66 | } 67 | 68 | func (m *Manage) NodeSort(less func(*node.Node, *node.Node) bool) { 69 | if m.NodeLen() <= 1 { 70 | return 71 | } 72 | for i := 1; i < m.NodeLen(); i++ { 73 | preIndex := i - 1 74 | current := m.getNode(i) 75 | for preIndex >= 0 && !less(m.getNode(preIndex), current) { 76 | m.NodeList[preIndex+1] = m.NodeList[preIndex] 77 | preIndex -= 1 78 | } 79 | m.NodeList[preIndex+1] = current 80 | } 81 | } 82 | 83 | func (m *Manage) Sort(mode int) { 84 | selectedNode := m.SelectedNode() 85 | switch mode { 86 | case 0: 87 | for i := 0; i < m.NodeLen()/2; i++ { 88 | j := m.NodeLen() - i - 1 89 | m.NodeList[i], m.NodeList[j] = m.NodeList[j], m.NodeList[i] 90 | } 91 | case 1: 92 | m.NodeSort(func(n1 *node.Node, n2 *node.Node) bool { 93 | return n1.GetProtocolMode() < n2.Protocol.GetProtocolMode() 94 | }) 95 | case 2: 96 | m.NodeSort(func(n1 *node.Node, n2 *node.Node) bool { 97 | return n1.GetName() < n2.GetName() 98 | }) 99 | case 3: 100 | m.NodeSort(func(n1 *node.Node, n2 *node.Node) bool { 101 | return n1.GetAddr() < n2.GetAddr() 102 | }) 103 | case 4: 104 | m.NodeSort(func(n1 *node.Node, n2 *node.Node) bool { 105 | return n1.GetPort() < n2.GetPort() 106 | }) 107 | case 5: 108 | m.NodeSort(func(n1 *node.Node, n2 *node.Node) bool { 109 | return n1.TestResult < n2.TestResult 110 | }) 111 | default: 112 | return 113 | } 114 | defer m.Save() 115 | m.SetSelectedIndexByNode(selectedNode) 116 | } 117 | 118 | func (m *Manage) DelNode(key string) { 119 | indexList := core.IndexList(key, m.NodeLen()) 120 | if len(indexList) == 0 { 121 | return 122 | } 123 | defer m.Save() 124 | selectedNode := m.SelectedNode() 125 | newNodeList := make([]*node.Node, 0) 126 | m.NodeForEach(func(i int, n *node.Node) { 127 | if HasIn(i, indexList) { 128 | m.MoveToRecycle(n) 129 | } else { 130 | newNodeList = append(newNodeList, n) 131 | } 132 | }) 133 | m.NodeList = newNodeList 134 | m.SetSelectedIndexByNode(selectedNode) 135 | } 136 | 137 | func (m *Manage) DelNodeById(id string) { 138 | defer m.Save() 139 | selectedNode := m.SelectedNode() 140 | newNodeList := make([]*node.Node, 0) 141 | m.NodeForEach(func(i int, n *node.Node) { 142 | if n.SubID == id { 143 | m.MoveToRecycle(n) 144 | } else { 145 | newNodeList = append(newNodeList, n) 146 | } 147 | }) 148 | m.NodeList = newNodeList 149 | m.SetSelectedIndexByNode(selectedNode) 150 | } 151 | 152 | func (m *Manage) GetNodeLink(key string) []string { 153 | links := make([]string, 0) 154 | for _, index := range core.IndexList(key, m.NodeLen()) { 155 | links = append(links, m.GetNode(index).GetLink()) 156 | } 157 | return links 158 | } 159 | -------------------------------------------------------------------------------- /core/protocols/vmessAEAD.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "Txray/core/protocols/field" 5 | "bytes" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | type VMessAEAD struct { 11 | ID string `json:"id"` 12 | Address string `json:"address"` 13 | Port int `json:"port"` 14 | Remarks string `json:"remarks"` 15 | url.Values 16 | } 17 | 18 | // GetProtocolMode 获取协议模式 19 | func (v *VMessAEAD) GetProtocolMode() Mode { 20 | return ModeVMessAEAD 21 | } 22 | 23 | // GetName 获取别名 24 | func (v *VMessAEAD) GetName() string { 25 | return v.Remarks 26 | } 27 | 28 | // GetAddr 获取远程地址 29 | func (v *VMessAEAD) GetAddr() string { 30 | return v.Address 31 | } 32 | 33 | // GetPort 获取远程端口 34 | func (v *VMessAEAD) GetPort() int { 35 | return v.Port 36 | } 37 | 38 | // GetInfo 获取节点数据 39 | func (v *VMessAEAD) GetInfo() string { 40 | var buf bytes.Buffer 41 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "别名", v.Remarks)) 42 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "地址", v.Address)) 43 | buf.WriteString(fmt.Sprintf("%7s: %d\n", "端口", v.Port)) 44 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "用户ID", v.ID)) 45 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "加密方式", v.GetValue(field.VMessEncryption))) 46 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "传输协议", v.GetValue(field.NetworkType))) 47 | switch v.GetValue(field.NetworkType) { 48 | case "tcp": 49 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装类型", v.GetValue(field.TCPHeaderType))) 50 | case "kcp": 51 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装类型", v.GetValue(field.MkcpHeaderType))) 52 | if v.GetValue(field.Seed) != "" { 53 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "KCP种子", v.GetValue(field.Seed))) 54 | } 55 | case "ws": 56 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Path", v.GetValue(field.WsPath))) 57 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Host", v.GetValue(field.WsHost))) 58 | case "h2": 59 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Path", v.GetValue(field.WsPath))) 60 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Host", v.GetHostValue(field.WsHost))) 61 | case "quic": 62 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装类型", v.GetValue(field.QuicHeaderType))) 63 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "QUIC加密", v.GetValue(field.QuicSecurity))) 64 | if v.GetValue(field.QuicSecurity) != "none" { 65 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "加密密钥", v.GetValue(field.QuicKey))) 66 | } 67 | case "grpc": 68 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "ServiceName", v.GetValue(field.GrpcServiceName))) 69 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "传输模式", v.GetValue(field.GrpcMode))) 70 | } 71 | if v.GetValue(field.Security) == "reality" { 72 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "FingerPrint", v.GetValue(field.FingerPrint))) 73 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "reality公钥", v.GetValue(field.PublicKey))) 74 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "realityID", v.GetValue(field.ShortId))) 75 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "REALITY爬虫", v.GetValue(field.SpiderX))) 76 | } 77 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "底层传输", v.GetValue(field.Security))) 78 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "SNI", v.GetValue(field.SNI))) 79 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Alpn", v.GetValue(field.Alpn))) 80 | buf.WriteString(fmt.Sprintf("%7s: %s", "协议", v.GetProtocolMode())) 81 | 82 | return buf.String() 83 | } 84 | 85 | // GetLink 获取节点分享链接 86 | func (v *VMessAEAD) GetLink() string { 87 | u := url.URL{ 88 | Scheme: "vmess", 89 | User: url.User(v.ID), 90 | Host: fmt.Sprintf("%s:%d", v.GetAddr(), v.GetPort()), 91 | RawQuery: v.Values.Encode(), 92 | Fragment: v.Remarks, 93 | } 94 | return u.String() 95 | } 96 | 97 | func (v *VMessAEAD) GetValue(field field.Field) string { 98 | if v.Has(field.Key) { 99 | return v.Get(field.Key) 100 | } 101 | return field.Value 102 | } 103 | 104 | // H2Host SNI 105 | func (v *VMessAEAD) GetHostValue(field field.Field) string { 106 | if v.Has(field.Key) { 107 | return v.Get(field.Key) 108 | } 109 | return v.Address 110 | } 111 | 112 | func (v *VMessAEAD) Check() *VMessAEAD { 113 | if v.ID != "" && v.Port > 0 && v.Port <= 65535 && v.Address != "" && v.Remarks != "" { 114 | return v 115 | } 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /core/protocols/vless.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "Txray/core/protocols/field" 5 | "bytes" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | type VLess struct { 11 | ID string `json:"id"` 12 | Address string `json:"address"` 13 | Port int `json:"port"` 14 | Remarks string `json:"remarks"` 15 | url.Values 16 | } 17 | 18 | // GetProtocolMode 获取协议模式 19 | func (v *VLess) GetProtocolMode() Mode { 20 | return ModeVLESS 21 | } 22 | 23 | // GetName 获取别名 24 | func (v *VLess) GetName() string { 25 | return v.Remarks 26 | } 27 | 28 | // GetAddr 获取远程地址 29 | func (v *VLess) GetAddr() string { 30 | return v.Address 31 | } 32 | 33 | // GetPort 获取远程端口 34 | func (v *VLess) GetPort() int { 35 | return v.Port 36 | } 37 | 38 | // GetInfo 获取节点数据 39 | func (v *VLess) GetInfo() string { 40 | var buf bytes.Buffer 41 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "别名", v.Remarks)) 42 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "地址", v.Address)) 43 | buf.WriteString(fmt.Sprintf("%7s: %d\n", "端口", v.Port)) 44 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "用户ID", v.ID)) 45 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "流控方式", v.GetValue(field.Flow))) 46 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "加密方式", v.GetValue(field.VLessEncryption))) 47 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "传输协议", v.GetValue(field.NetworkType))) 48 | switch v.GetValue(field.NetworkType) { 49 | case "tcp": 50 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装类型", v.GetValue(field.TCPHeaderType))) 51 | case "kcp": 52 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装类型", v.GetValue(field.MkcpHeaderType))) 53 | if v.GetValue(field.Seed) != "" { 54 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "KCP种子", v.GetValue(field.Seed))) 55 | } 56 | case "ws": 57 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Path", v.GetValue(field.WsPath))) 58 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Host", v.GetValue(field.WsHost))) 59 | case "h2": 60 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Path", v.GetValue(field.WsPath))) 61 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Host", v.GetHostValue(field.WsHost))) 62 | case "quic": 63 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "伪装类型", v.GetValue(field.QuicHeaderType))) 64 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "QUIC加密", v.GetValue(field.QuicSecurity))) 65 | if v.GetValue(field.QuicSecurity) != "none" { 66 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "加密密钥", v.GetValue(field.QuicKey))) 67 | } 68 | case "grpc": 69 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "ServiceName", v.GetValue(field.GrpcServiceName))) 70 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "传输模式", v.GetValue(field.GrpcMode))) 71 | } 72 | if v.GetValue(field.Security) == "reality" { 73 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "FingerPrint", v.GetValue(field.FingerPrint))) 74 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "reality公钥", v.GetValue(field.PublicKey))) 75 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "realityID", v.GetValue(field.ShortId))) 76 | buf.WriteString(fmt.Sprintf("%7s: %s\n", "REALITY爬虫", v.GetValue(field.SpiderX))) 77 | } 78 | buf.WriteString(fmt.Sprintf("%5s: %s\n", "底层传输", v.GetValue(field.Security))) 79 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "SNI", v.GetValue(field.SNI))) 80 | buf.WriteString(fmt.Sprintf("%9s: %s\n", "Alpn", v.GetValue(field.Alpn))) 81 | buf.WriteString(fmt.Sprintf("%7s: %s", "协议", v.GetProtocolMode())) 82 | 83 | return buf.String() 84 | } 85 | 86 | // GetLink 获取节点分享链接 87 | func (v *VLess) GetLink() string { 88 | u := url.URL{ 89 | Scheme: "vless", 90 | User: url.User(v.ID), 91 | Host: fmt.Sprintf("%s:%d", v.GetAddr(), v.GetPort()), 92 | RawQuery: v.Values.Encode(), 93 | Fragment: v.Remarks, 94 | } 95 | return u.String() 96 | } 97 | 98 | func (v *VLess) GetValue(field field.Field) string { 99 | if v.Has(field.Key) { 100 | return v.Get(field.Key) 101 | } 102 | return field.Value 103 | } 104 | 105 | // H2Host SNI 106 | func (v *VLess) GetHostValue(field field.Field) string { 107 | if v.Has(field.Key) { 108 | return v.Get(field.Key) 109 | } 110 | return v.Address 111 | } 112 | 113 | func (v *VLess) Check() *VLess { 114 | if v.ID != "" && v.Port > 0 && v.Port <= 65535 && v.Address != "" && v.Remarks != "" { 115 | return v 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /xray/service.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/manage" 6 | "Txray/core/protocols" 7 | "Txray/core/setting" 8 | "Txray/log" 9 | "bufio" 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | "time" 14 | 15 | "github.com/hpcloud/tail" 16 | ) 17 | 18 | var Xray *exec.Cmd 19 | 20 | func Start(key string) { 21 | testUrl := setting.TestUrl() 22 | testTimeout := setting.TestTimeout() 23 | manager := manage.Manager 24 | indexList := core.IndexList(key, manager.NodeLen()) 25 | if len(indexList) == 0 { 26 | log.Warn("没有选取到节点") 27 | } else if len(indexList) == 1 { 28 | index := indexList[0] 29 | node := manager.GetNode(index) 30 | manager.SetSelectedIndex(index) 31 | manager.Save() 32 | exe := run(node.Protocol) 33 | if exe { 34 | if setting.Http() == 0 { 35 | log.Infof("启动成功, 监听socks端口: %d, 所选节点: %d", setting.Socks(), manager.SelectedIndex()) 36 | } else { 37 | log.Infof("启动成功, 监听socks/http端口: %d/%d, 所选节点: %d", setting.Socks(), setting.Http(), manager.SelectedIndex()) 38 | } 39 | result, status := TestNode(testUrl, setting.Socks(), testTimeout) 40 | log.Infof("%6s [ %s ] 延迟: %dms", status, testUrl, result) 41 | } 42 | } else { 43 | min := 100000 44 | i := -1 45 | for _, index := range indexList { 46 | node := manager.GetNode(index) 47 | exe := run(node.Protocol) 48 | if exe { 49 | result, status := TestNode(testUrl, setting.Socks(), testTimeout) 50 | log.Infof("%6s [ %s ] 节点: %d, 延迟: %dms", status, testUrl, index, result) 51 | if result > 0 && result <= setting.TestMinTime(){ 52 | i = index 53 | min = result 54 | break 55 | } 56 | if result != -1 && min > result { 57 | i = index 58 | min = result 59 | } 60 | } else { 61 | return 62 | } 63 | } 64 | if i != -1 { 65 | log.Info("延迟最小的节点为:", i, ",延迟:", min, "ms") 66 | manager.SetSelectedIndex(i) 67 | manager.Save() 68 | node := manager.GetNode(i) 69 | exe := run(node.Protocol) 70 | if exe { 71 | if setting.Http() == 0 { 72 | log.Infof("启动成功, 监听socks端口: %d, 所选节点: %d", setting.Socks(), manager.SelectedIndex()) 73 | } else { 74 | log.Infof("启动成功, 监听socks/http端口: %d/%d, 所选节点: %d", setting.Socks(), setting.Http(), manager.SelectedIndex()) 75 | } 76 | } else { 77 | log.Error("启动失败") 78 | } 79 | } else { 80 | log.Info("所选节点全部不能访问外网") 81 | } 82 | 83 | } 84 | } 85 | 86 | func run(node protocols.Protocol) bool { 87 | Stop() 88 | switch node.GetProtocolMode() { 89 | case protocols.ModeShadowSocks, protocols.ModeTrojan, protocols.ModeVMess, protocols.ModeSocks, protocols.ModeVLESS, protocols.ModeVMessAEAD: 90 | file := GenConfig(node) 91 | Xray = exec.Command(XrayPath, "-c", file) 92 | default: 93 | log.Infof("暂不支持%v协议", node.GetProtocolMode()) 94 | return false 95 | } 96 | stdout, _ := Xray.StdoutPipe() 97 | _ = Xray.Start() 98 | r := bufio.NewReader(stdout) 99 | lines := new([]string) 100 | go readInfo(r, lines) 101 | status := make(chan struct{}) 102 | go checkProc(Xray, status) 103 | stopper := time.NewTimer(time.Millisecond * 300) 104 | select { 105 | case <-stopper.C: 106 | setting.SetPid(Xray.Process.Pid) 107 | return true 108 | case <-status: 109 | log.Error("开启xray服务失败, 查看下面报错信息来检查出错问题") 110 | for _, x := range *lines { 111 | log.Error(x) 112 | } 113 | return false 114 | } 115 | } 116 | 117 | // Stop 停止服务 118 | func Stop() { 119 | if Xray != nil { 120 | Xray.Process.Kill() 121 | Xray = nil 122 | } 123 | if setting.Pid() != 0 { 124 | process, err := os.FindProcess(setting.Pid()) 125 | if err == nil { 126 | process.Kill() 127 | } 128 | setting.SetPid(0) 129 | } 130 | // 日志文件过大清除 131 | file, _ := os.Stat(core.LogFile) 132 | if file != nil { 133 | fileSize := float64(file.Size())/ (1 << 20) 134 | if fileSize > 5 { 135 | os.Remove(core.LogFile) 136 | } 137 | } 138 | } 139 | 140 | // 查看xray日志 141 | func ShowLog() { 142 | t, _ := tail.TailFile(core.LogFile, tail.Config{ 143 | ReOpen: true, // 重新打开 144 | Follow: true, // 是否跟随 145 | Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个地方开始读 146 | MustExist: false, // 文件不存在不报错 147 | Poll: true, 148 | }) 149 | for line := range t.Lines { 150 | fmt.Println(line.Text) 151 | } 152 | } 153 | 154 | func readInfo(r *bufio.Reader, lines *[]string) { 155 | for i := 0; i < 20; i++ { 156 | line, _, _ := r.ReadLine() 157 | if len(string(line[:])) != 0 { 158 | *lines = append(*lines, string(line[:])) 159 | } 160 | } 161 | } 162 | 163 | // 检查进程状态 164 | func checkProc(c *exec.Cmd, status chan struct{}) { 165 | c.Wait() 166 | status <- struct{}{} 167 | } -------------------------------------------------------------------------------- /cmd/routing.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/core/routing" 6 | "Txray/log" 7 | "fmt" 8 | "github.com/abiosoft/ishell" 9 | "github.com/atotto/clipboard" 10 | "github.com/olekukonko/tablewriter" 11 | "io/ioutil" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | func InitRouteShell(shell *ishell.Shell) { 17 | routingCmd := &ishell.Cmd{ 18 | Name: "routing", 19 | Aliases: []string{"-h", "--help"}, 20 | Func: func(c *ishell.Context) { 21 | shell.Process("routing", "help") 22 | }, 23 | } 24 | routingCmd.AddCmd(&ishell.Cmd{ 25 | Name: "help", 26 | Func: func(c *ishell.Context) { 27 | c.Println(help.Routing) 28 | }, 29 | }) 30 | // block 31 | routingCmd.AddCmd(&ishell.Cmd{ 32 | Name: "block", 33 | Func: func(c *ishell.Context) { 34 | argMap := FlagsParse(c.Args, map[string]string{ 35 | "a": "add", 36 | "r": "rm", 37 | "f": "file", 38 | "c": "clipboard", 39 | }) 40 | mode := routing.TypeBlock 41 | if _, ok := argMap["clipboard"]; ok { 42 | content, err := clipboard.ReadAll() 43 | if err != nil { 44 | log.Error(err) 45 | return 46 | } 47 | content = strings.ReplaceAll(content, "\r\n", "\n") 48 | content = strings.ReplaceAll(content, "\r", "\n") 49 | routing.AddRule(mode, strings.Split(content, "\n")...) 50 | } else if fileArg, ok := argMap["file"]; ok { 51 | if _, err := os.Stat(fileArg); os.IsNotExist(err) { 52 | log.Error("open ", fileArg, " : 没有这个文件") 53 | return 54 | } 55 | data, _ := ioutil.ReadFile(fileArg) 56 | content := strings.ReplaceAll(string(data), "\r\n", "\n") 57 | content = strings.ReplaceAll(content, "\r", "\n") 58 | count := routing.AddRule(mode, strings.Split(content, "\n")...) 59 | log.Infof("共添加了%d条规则", count) 60 | } else if data, ok := argMap["add"]; ok { 61 | routing.AddRule(mode, data) 62 | } else if key, ok := argMap["rm"]; ok { 63 | routing.DelRule(mode, key) 64 | } else if key, ok := argMap["data"]; ok { 65 | table := tablewriter.NewWriter(os.Stdout) 66 | table.SetHeader([]string{"索引", "类型", "规则"}) 67 | center := tablewriter.ALIGN_CENTER 68 | table.SetAlignment(center) 69 | table.AppendBulk(routing.GetRule(mode, key)) 70 | table.SetCaption(true, fmt.Sprintf("共[ %d ]条规则", routing.RuleLen(mode))) 71 | table.Render() 72 | } else { 73 | table := tablewriter.NewWriter(os.Stdout) 74 | table.SetHeader([]string{"索引", "类型", "规则"}) 75 | center := tablewriter.ALIGN_CENTER 76 | table.SetAlignment(center) 77 | table.AppendBulk(routing.GetRule(mode, "0-100")) 78 | table.SetCaption(true, fmt.Sprintf("共[ %d ]条规则", routing.RuleLen(mode))) 79 | table.Render() 80 | } 81 | }, 82 | }) 83 | // proxy 84 | routingCmd.AddCmd(&ishell.Cmd{ 85 | Name: "proxy", 86 | Func: func(c *ishell.Context) { 87 | argMap := FlagsParse(c.Args, map[string]string{ 88 | "a": "add", 89 | "r": "rm", 90 | "f": "file", 91 | "c": "clipboard", 92 | }) 93 | mode := routing.TypeProxy 94 | if _, ok := argMap["clipboard"]; ok { 95 | content, err := clipboard.ReadAll() 96 | if err != nil { 97 | log.Error(err) 98 | return 99 | } 100 | content = strings.ReplaceAll(content, "\r\n", "\n") 101 | content = strings.ReplaceAll(content, "\r", "\n") 102 | routing.AddRule(mode, strings.Split(content, "\n")...) 103 | } else if fileArg, ok := argMap["file"]; ok { 104 | if _, err := os.Stat(fileArg); os.IsNotExist(err) { 105 | log.Error("open ", fileArg, " : 没有这个文件") 106 | return 107 | } 108 | data, _ := ioutil.ReadFile(fileArg) 109 | content := strings.ReplaceAll(string(data), "\r\n", "\n") 110 | content = strings.ReplaceAll(content, "\r", "\n") 111 | count := routing.AddRule(mode, strings.Split(content, "\n")...) 112 | log.Infof("共添加了%d条规则", count) 113 | } else if data, ok := argMap["add"]; ok { 114 | routing.AddRule(mode, data) 115 | } else if key, ok := argMap["rm"]; ok { 116 | routing.DelRule(mode, key) 117 | } else if key, ok := argMap["data"]; ok { 118 | table := tablewriter.NewWriter(os.Stdout) 119 | table.SetHeader([]string{"索引", "类型", "规则"}) 120 | center := tablewriter.ALIGN_CENTER 121 | table.SetAlignment(center) 122 | table.AppendBulk(routing.GetRule(mode, key)) 123 | table.SetCaption(true, fmt.Sprintf("共[ %d ]条规则", routing.RuleLen(mode))) 124 | table.Render() 125 | } else { 126 | table := tablewriter.NewWriter(os.Stdout) 127 | table.SetHeader([]string{"索引", "类型", "规则"}) 128 | center := tablewriter.ALIGN_CENTER 129 | table.SetAlignment(center) 130 | table.AppendBulk(routing.GetRule(mode, "0-100")) 131 | table.SetCaption(true, fmt.Sprintf("共[ %d ]条规则", routing.RuleLen(mode))) 132 | table.Render() 133 | } 134 | }, 135 | }) 136 | // direct 137 | routingCmd.AddCmd(&ishell.Cmd{ 138 | Name: "direct", 139 | Func: func(c *ishell.Context) { 140 | argMap := FlagsParse(c.Args, map[string]string{ 141 | "a": "add", 142 | "r": "rm", 143 | "f": "file", 144 | "c": "clipboard", 145 | }) 146 | mode := routing.TypeDirect 147 | if _, ok := argMap["clipboard"]; ok { 148 | content, err := clipboard.ReadAll() 149 | if err != nil { 150 | log.Error(err) 151 | return 152 | } 153 | content = strings.ReplaceAll(content, "\r\n", "\n") 154 | content = strings.ReplaceAll(content, "\r", "\n") 155 | routing.AddRule(mode, strings.Split(content, "\n")...) 156 | } else if fileArg, ok := argMap["file"]; ok { 157 | if _, err := os.Stat(fileArg); os.IsNotExist(err) { 158 | log.Error("open ", fileArg, " : 没有这个文件") 159 | return 160 | } 161 | data, _ := ioutil.ReadFile(fileArg) 162 | content := strings.ReplaceAll(string(data), "\r\n", "\n") 163 | content = strings.ReplaceAll(content, "\r", "\n") 164 | count := routing.AddRule(mode, strings.Split(content, "\n")...) 165 | log.Infof("共添加了%d条规则", count) 166 | } else if data, ok := argMap["add"]; ok { 167 | routing.AddRule(mode, data) 168 | } else if key, ok := argMap["rm"]; ok { 169 | routing.DelRule(mode, key) 170 | } else if key, ok := argMap["data"]; ok { 171 | table := tablewriter.NewWriter(os.Stdout) 172 | table.SetHeader([]string{"索引", "类型", "规则"}) 173 | center := tablewriter.ALIGN_CENTER 174 | table.SetAlignment(center) 175 | table.AppendBulk(routing.GetRule(mode, key)) 176 | table.SetCaption(true, fmt.Sprintf("共[ %d ]条规则", routing.RuleLen(mode))) 177 | table.Render() 178 | } else { 179 | table := tablewriter.NewWriter(os.Stdout) 180 | table.SetHeader([]string{"索引", "类型", "规则"}) 181 | center := tablewriter.ALIGN_CENTER 182 | table.SetAlignment(center) 183 | table.AppendBulk(routing.GetRule(mode, "0-100")) 184 | table.SetCaption(true, fmt.Sprintf("共[ %d ]条规则", routing.RuleLen(mode))) 185 | table.Render() 186 | } 187 | }, 188 | }) 189 | shell.AddCmd(routingCmd) 190 | } 191 | -------------------------------------------------------------------------------- /core/protocols/parse.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // 解析链接 13 | func ParseLink(link string) Protocol { 14 | u, err := url.Parse(link) 15 | if err != nil { 16 | return nil 17 | } 18 | switch u.Scheme { 19 | case "vmess": 20 | if obj := ParseVMessLink(link); obj != nil { 21 | return obj 22 | } 23 | if obj := ParseVMessAEADLink(link); obj != nil { 24 | return obj 25 | } 26 | case "vless": 27 | if obj := ParseVLessLink(link); obj != nil { 28 | return obj 29 | } 30 | case "ss": 31 | if obj := ParseSSLink(link); obj != nil { 32 | return obj 33 | } 34 | case "ssr": 35 | if obj := ParseSSRLink(link); obj != nil { 36 | return obj 37 | } 38 | case "trojan": 39 | if obj := ParseTrojanLink(link); obj != nil { 40 | return obj 41 | } 42 | case "socks": 43 | if obj := ParseSocksLink(link); obj != nil { 44 | return obj 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func ParseVMessAEADLink(link string) *VMessAEAD { 51 | u, err := url.Parse(link) 52 | if err != nil { 53 | return nil 54 | } 55 | if u.Scheme != "vmess" { 56 | return nil 57 | } 58 | vless := new(VMessAEAD) 59 | vless.Address = u.Hostname() 60 | vless.Port, err = strconv.Atoi(u.Port()) 61 | if err != nil { 62 | return nil 63 | } 64 | if u.User == nil { 65 | return nil 66 | } 67 | vless.ID = u.User.Username() 68 | vless.Remarks = u.Fragment 69 | vless.Values = u.Query() 70 | if vless.Remarks == "" { 71 | vless.Remarks = u.Host 72 | } 73 | return vless.Check() 74 | } 75 | 76 | func ParseVLessLink(link string) *VLess { 77 | u, err := url.Parse(link) 78 | if err != nil { 79 | return nil 80 | } 81 | if u.Scheme != "vless" { 82 | return nil 83 | } 84 | vless := new(VLess) 85 | vless.Address = u.Hostname() 86 | vless.Port, err = strconv.Atoi(u.Port()) 87 | if err != nil { 88 | return nil 89 | } 90 | if u.User == nil { 91 | return nil 92 | } 93 | vless.ID = u.User.Username() 94 | vless.Remarks = u.Fragment 95 | vless.Values = u.Query() 96 | if vless.Remarks == "" { 97 | vless.Remarks = u.Host 98 | } 99 | return vless.Check() 100 | } 101 | 102 | func ParseSocksLink(link string) *Socks { 103 | u, err := url.Parse(link) 104 | if err != nil { 105 | return nil 106 | } 107 | if u.Scheme != "socks" { 108 | return nil 109 | } 110 | socks := new(Socks) 111 | 112 | socks.Address = u.Hostname() 113 | socks.Port, err = strconv.Atoi(u.Port()) 114 | if err != nil { 115 | return nil 116 | } 117 | if u.User != nil { 118 | socks.Username = u.User.Username() 119 | socks.Password, _ = u.User.Password() 120 | } 121 | socks.Remarks = u.Fragment 122 | if socks.Remarks == "" { 123 | socks.Remarks = u.Host 124 | } 125 | return socks.Check() 126 | } 127 | 128 | func ParseVMessLink(link string) *VMess { 129 | vmess := new(VMess) 130 | if strings.ToLower(link[:8]) == "vmess://" { 131 | link = link[8:] 132 | } else { 133 | return nil 134 | } 135 | if len(link) == 0 { 136 | return nil 137 | } 138 | jsonStr := base64Decode(link) 139 | if jsonStr == "" { 140 | return nil 141 | } 142 | var mapResult map[string]interface{} 143 | err := json.Unmarshal([]byte(jsonStr), &mapResult) 144 | if err != nil { 145 | return nil 146 | } 147 | if version, ok := mapResult["v"]; ok { 148 | vmess.V = fmt.Sprintf("%v", version) 149 | } 150 | if ps, ok := mapResult["ps"]; ok { 151 | vmess.Ps = fmt.Sprintf("%v", ps) 152 | } else { 153 | return nil 154 | } 155 | if addr, ok := mapResult["add"]; ok { 156 | vmess.Add = fmt.Sprintf("%v", addr) 157 | } else { 158 | return nil 159 | } 160 | if scy, ok := mapResult["scy"]; ok { 161 | vmess.Scy = fmt.Sprintf("%v", scy) 162 | } else { 163 | vmess.Scy = "auto" 164 | } 165 | if port, ok := mapResult["port"]; ok { 166 | value, err := strconv.Atoi(fmt.Sprintf("%v", port)) 167 | if err == nil { 168 | vmess.Port = value 169 | } else { 170 | return nil 171 | } 172 | } else { 173 | return nil 174 | } 175 | 176 | if id, ok := mapResult["id"]; ok { 177 | vmess.Id = fmt.Sprintf("%v", id) 178 | } else { 179 | return nil 180 | } 181 | if aid, ok := mapResult["aid"]; ok { 182 | if value, err := strconv.Atoi(fmt.Sprintf("%v", aid)); err == nil { 183 | vmess.Aid = value 184 | } else { 185 | return nil 186 | } 187 | } else { 188 | return nil 189 | } 190 | if net, ok := mapResult["net"]; ok { 191 | vmess.Net = fmt.Sprintf("%v", net) 192 | } else { 193 | return nil 194 | } 195 | if type1, ok := mapResult["type"]; ok { 196 | vmess.Type = fmt.Sprintf("%v", type1) 197 | } else { 198 | return nil 199 | } 200 | if host, ok := mapResult["host"]; ok { 201 | vmess.Host = fmt.Sprintf("%v", host) 202 | } else { 203 | return nil 204 | } 205 | if path, ok := mapResult["path"]; ok { 206 | vmess.Path = fmt.Sprintf("%v", path) 207 | } else { 208 | return nil 209 | } 210 | if tls, ok := mapResult["tls"]; ok { 211 | vmess.Tls = fmt.Sprintf("%v", tls) 212 | } else { 213 | return nil 214 | } 215 | if sni, ok := mapResult["sni"]; ok { 216 | vmess.Sni = fmt.Sprintf("%v", sni) 217 | } 218 | if alpn, ok := mapResult["alpn"]; ok { 219 | vmess.Alpn = fmt.Sprintf("%v", alpn) 220 | } 221 | return vmess.Check() 222 | } 223 | 224 | func ParseTrojanLink(link string) *Trojan { 225 | u, err := url.Parse(link) 226 | if err != nil { 227 | return nil 228 | } 229 | if u.Scheme != "trojan" { 230 | return nil 231 | } 232 | trojan := new(Trojan) 233 | if u.User == nil { 234 | return nil 235 | } 236 | trojan.Password = u.User.Username() 237 | trojan.Address = u.Hostname() 238 | trojan.Port, err = strconv.Atoi(u.Port()) 239 | if err != nil { 240 | return nil 241 | } 242 | trojan.Remarks = u.Fragment 243 | if trojan.Remarks == "" { 244 | trojan.Remarks = u.Host 245 | } 246 | trojan.Values = u.Query() 247 | return trojan.Check() 248 | } 249 | 250 | func ParseSSRLink(link string) *ShadowSocksR { 251 | ssr := new(ShadowSocksR) 252 | if strings.ToLower(link[:6]) == "ssr://" { 253 | link = link[6:] 254 | } else { 255 | return nil 256 | } 257 | link = base64Decode(link) 258 | if link == "" { 259 | return nil 260 | } 261 | expr := `^([a-zA-Z0-9-\.]*):([0-9]{1,5}):([a-z0-9_]*):([a-z0-9-]*):([a-z0-9_\.]*):([a-zA-Z0-9-_=]*)(/.(.*$))?` 262 | r, _ := regexp.Compile(expr) 263 | result := r.FindStringSubmatch(link) 264 | if len(result) != 9 { 265 | return nil 266 | } 267 | ssr.Address = result[1] 268 | ssr.Port, _ = strconv.Atoi(result[2]) 269 | if ssr.Port < 0 || ssr.Port > 65535 { 270 | return nil 271 | } 272 | ssr.Protocol = result[3] 273 | ssr.Method = result[4] 274 | ssr.Obfs = result[5] 275 | ssr.Password = base64Decode(result[6]) 276 | if ssr.Password == "" { 277 | return nil 278 | } 279 | for _, str := range strings.Split(result[8], "&") { 280 | if strings.HasPrefix(str, "obfsparam=") { 281 | ssr.ObfsParam = base64Decode(str[10:]) 282 | } else if strings.HasPrefix(str, "protoparam=") { 283 | ssr.ProtoParam = base64Decode(str[11:]) 284 | } else if strings.HasPrefix(str, "remarks=") { 285 | ssr.Remarks = base64Decode(str[8:]) 286 | } else if strings.HasPrefix(str, "group=") { 287 | ssr.Group = base64Decode(str[6:]) 288 | } 289 | } 290 | if ssr.Remarks == "" { 291 | ssr.Remarks = ssr.Address + ":" + strconv.Itoa(ssr.Port) 292 | } 293 | return ssr 294 | } 295 | 296 | func ParseSSLink(link string) *ShadowSocks { 297 | u, err := url.Parse(link) 298 | if err != nil { 299 | return nil 300 | } 301 | if u.Scheme != "ss" { 302 | return nil 303 | } 304 | ss := new(ShadowSocks) 305 | ss.Remarks = u.Fragment 306 | if u.User == nil { 307 | u, err = url.Parse("ss://" + base64Decode(u.Host)) 308 | if err != nil { 309 | return nil 310 | } 311 | ss.Address = u.Hostname() 312 | ss.Port, _ = strconv.Atoi(u.Port()) 313 | ss.Method = u.User.Username() 314 | ss.Password, _ = u.User.Password() 315 | if ss.Remarks == "" { 316 | ss.Remarks = u.Host 317 | } 318 | } else { 319 | ss.Address = u.Hostname() 320 | ss.Port, err = strconv.Atoi(u.Port()) 321 | if err != nil { 322 | return nil 323 | } 324 | if ss.Remarks == "" { 325 | ss.Remarks = u.Host 326 | } 327 | result := strings.SplitN(base64Decode(u.User.Username()), ":", 2) 328 | ss.Method = result[0] 329 | ss.Password = result[1] 330 | ss.Values = u.Query() 331 | } 332 | return ss.Check() 333 | } 334 | -------------------------------------------------------------------------------- /cmd/setting.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/core/setting" 6 | "Txray/log" 7 | "github.com/abiosoft/ishell" 8 | "github.com/olekukonko/tablewriter" 9 | "os" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | func InitSettingShell(shell *ishell.Shell) { 15 | baseSettingCmd := &ishell.Cmd{ 16 | Name: "setting", 17 | Func: func(c *ishell.Context) { 18 | // 连接设置 19 | table := tablewriter.NewWriter(os.Stdout) 20 | table.SetHeader([]string{"socks端口", "http端口", "udp转发", "流量地址监听", "允许来自局域网连接", "多路复用", "允许不安全的连接"}) 21 | table.SetAlignment(tablewriter.ALIGN_CENTER) 22 | data := []string{ 23 | strconv.Itoa(setting.Socks()), 24 | strconv.Itoa(setting.Http()), 25 | strconv.FormatBool(setting.UDP()), 26 | strconv.FormatBool(setting.Sniffing()), 27 | strconv.FormatBool(setting.FromLanConn()), 28 | strconv.FormatBool(setting.Mux()), 29 | strconv.FormatBool(setting.AllowInsecure()), 30 | } 31 | table.Append(data) 32 | table.Render() 33 | 34 | // DNS及路由设置 35 | table = tablewriter.NewWriter(os.Stdout) 36 | table.SetHeader([]string{"DNS端口", "国外DNS", "国内DNS", "备用国内DNS", "路由策略", "绕过局域网和大陆"}) 37 | table.SetAlignment(tablewriter.ALIGN_CENTER) 38 | data = []string{ 39 | strconv.Itoa(setting.DNSPort()), 40 | setting.DNSForeign(), 41 | setting.DNSDomestic(), 42 | setting.DNSBackup(), 43 | setting.RoutingStrategy(), 44 | strconv.FormatBool(setting.RoutingBypass()), 45 | } 46 | table.Append(data) 47 | table.Render() 48 | 49 | table = tablewriter.NewWriter(os.Stdout) 50 | table.SetHeader([]string{"测试国外URL", "测试超时时间 (秒)", "批量测试终止时间 (毫秒)", "启动时执行"}) 51 | table.SetAlignment(tablewriter.ALIGN_CENTER) 52 | data = []string{ 53 | setting.TestUrl(), 54 | strconv.Itoa(setting.TestTimeout()), 55 | strconv.Itoa(setting.TestMinTime()), 56 | setting.RunBefore(), 57 | } 58 | table.Append(data) 59 | table.Render() 60 | }, 61 | } 62 | baseSettingCmd.AddCmd(&ishell.Cmd{ 63 | Name: "help", 64 | Aliases: []string{"-h", "--help"}, 65 | Func: func(c *ishell.Context) { 66 | c.Println(help.Setting) 67 | }, 68 | }) 69 | 70 | // 本地连接设置 71 | baseSettingCmd.AddCmd(&ishell.Cmd{ 72 | Name: "socks", 73 | Func: func(c *ishell.Context) { 74 | if len(c.Args) > 0 { 75 | v, err := strconv.Atoi(c.Args[0]) 76 | if err != nil { 77 | log.Warn("非法输入") 78 | return 79 | } 80 | err = setting.SetSocks(v) 81 | if err != nil { 82 | log.Error(err) 83 | return 84 | } 85 | } 86 | log.Info("socks端口: ", setting.Socks()) 87 | }, 88 | }) 89 | baseSettingCmd.AddCmd(&ishell.Cmd{ 90 | Name: "http", 91 | Func: func(c *ishell.Context) { 92 | if len(c.Args) > 0 { 93 | v, err := strconv.Atoi(c.Args[0]) 94 | if err != nil { 95 | log.Error("非法输入") 96 | return 97 | } 98 | err = setting.SetHttp(v) 99 | if err != nil { 100 | log.Error(err) 101 | return 102 | } 103 | } 104 | log.Info("http端口: ", setting.Http()) 105 | }, 106 | }) 107 | baseSettingCmd.AddCmd(&ishell.Cmd{ 108 | Name: "udp", 109 | Func: func(c *ishell.Context) { 110 | if len(c.Args) > 0 { 111 | str := strings.ToLower(c.Args[0]) 112 | switch str { 113 | case "y", "yes", "true", "t": 114 | setting.SetUDP(true) 115 | case "n", "no", "false", "f": 116 | setting.SetUDP(false) 117 | } 118 | } 119 | log.Info("UDP转发: ", setting.UDP()) 120 | }, 121 | }) 122 | baseSettingCmd.AddCmd(&ishell.Cmd{ 123 | Name: "sniffing", 124 | Func: func(c *ishell.Context) { 125 | if len(c.Args) > 0 { 126 | str := strings.ToLower(c.Args[0]) 127 | switch str { 128 | case "y", "yes", "true", "t": 129 | setting.SetSniffing(true) 130 | case "n", "no", "false", "f": 131 | setting.SetSniffing(false) 132 | } 133 | } 134 | log.Info("流量地址监听: ", setting.Sniffing()) 135 | }, 136 | }) 137 | baseSettingCmd.AddCmd(&ishell.Cmd{ 138 | Name: "mux", 139 | Func: func(c *ishell.Context) { 140 | if len(c.Args) > 0 { 141 | str := strings.ToLower(c.Args[0]) 142 | switch str { 143 | case "y", "yes", "true", "t": 144 | setting.SetMux(true) 145 | case "n", "no", "false", "f": 146 | setting.SetMux(false) 147 | } 148 | } 149 | log.Info("多路复用: ", setting.Mux()) 150 | }, 151 | }) 152 | baseSettingCmd.AddCmd(&ishell.Cmd{ 153 | Name: "allow_insecure", 154 | Func: func(c *ishell.Context) { 155 | if len(c.Args) > 0 { 156 | str := strings.ToLower(c.Args[0]) 157 | switch str { 158 | case "y", "yes", "true", "t": 159 | setting.SetAllowInsecure(true) 160 | case "n", "no", "false", "f": 161 | setting.SetAllowInsecure(false) 162 | } 163 | } 164 | log.Info("允许不安全的连接: ", setting.AllowInsecure()) 165 | }, 166 | }) 167 | baseSettingCmd.AddCmd(&ishell.Cmd{ 168 | Name: "from_lan_conn", 169 | Func: func(c *ishell.Context) { 170 | if len(c.Args) > 0 { 171 | str := strings.ToLower(c.Args[0]) 172 | switch str { 173 | case "y", "yes", "true", "t": 174 | setting.SetFromLanConn(true) 175 | case "n", "no", "false", "f": 176 | setting.SetFromLanConn(false) 177 | } 178 | } 179 | log.Info("来自局域网连接: ", setting.FromLanConn()) 180 | }, 181 | }) 182 | 183 | // 路由 184 | baseSettingCmd.AddCmd(&ishell.Cmd{ 185 | Name: "routing.strategy", 186 | Func: func(c *ishell.Context) { 187 | if len(c.Args) > 0 { 188 | switch c.Args[0] { 189 | case "1", "AsIs": 190 | setting.SetRoutingStrategy(1) 191 | case "2", "RoutingStrategy": 192 | setting.SetRoutingStrategy(2) 193 | case "3", "IPOnDemand": 194 | setting.SetRoutingStrategy(3) 195 | } 196 | } 197 | log.Info("路由策略: ", setting.RoutingStrategy()) 198 | }, 199 | }) 200 | baseSettingCmd.AddCmd(&ishell.Cmd{ 201 | Name: "routing.bypass", 202 | Func: func(c *ishell.Context) { 203 | if len(c.Args) > 0 { 204 | str := strings.ToLower(c.Args[0]) 205 | switch str { 206 | case "y", "yes", "true", "t": 207 | setting.SetRoutingBypass(true) 208 | case "n", "no", "false", "f": 209 | setting.SetRoutingBypass(false) 210 | } 211 | } 212 | log.Info("绕过局域网和大陆: ", setting.RoutingBypass()) 213 | }, 214 | }) 215 | 216 | // DNS 217 | baseSettingCmd.AddCmd(&ishell.Cmd{ 218 | Name: "dns.port", 219 | Func: func(c *ishell.Context) { 220 | if len(c.Args) > 0 { 221 | v, err := strconv.Atoi(c.Args[0]) 222 | if err != nil { 223 | log.Error("非法输入") 224 | return 225 | } 226 | err = setting.SetDNSPort(v) 227 | if err != nil { 228 | log.Error(err) 229 | return 230 | } 231 | } 232 | log.Info("DNS端口: ", setting.DNSPort()) 233 | }, 234 | }) 235 | baseSettingCmd.AddCmd(&ishell.Cmd{ 236 | Name: "dns.foreign", 237 | Func: func(c *ishell.Context) { 238 | if len(c.Args) > 0 { 239 | err := setting.SetDNSForeign(c.Args[0]) 240 | if err != nil { 241 | log.Warn(err) 242 | return 243 | } 244 | } 245 | log.Info("国外DNS: ", setting.DNSForeign()) 246 | }, 247 | }) 248 | baseSettingCmd.AddCmd(&ishell.Cmd{ 249 | Name: "dns.domestic", 250 | Func: func(c *ishell.Context) { 251 | if len(c.Args) > 0 { 252 | err := setting.SetDNSDomestic(c.Args[0]) 253 | if err != nil { 254 | log.Warn(err) 255 | return 256 | } 257 | } 258 | log.Info("国内DNS: ", setting.DNSDomestic()) 259 | }, 260 | }) 261 | baseSettingCmd.AddCmd(&ishell.Cmd{ 262 | Name: "dns.backup", 263 | Func: func(c *ishell.Context) { 264 | if len(c.Args) > 0 { 265 | err := setting.SetDNSBackup(c.Args[0]) 266 | if err != nil { 267 | log.Warn(err) 268 | return 269 | } 270 | } 271 | log.Info("备用国内DNS: ", setting.DNSBackup()) 272 | }, 273 | }) 274 | 275 | // 外网测试设置 276 | baseSettingCmd.AddCmd(&ishell.Cmd{ 277 | Name: "test.timeout", 278 | Func: func(c *ishell.Context) { 279 | if len(c.Args) > 0 { 280 | v, err := strconv.Atoi(c.Args[0]) 281 | if err != nil { 282 | log.Error("非法输入") 283 | return 284 | } 285 | err = setting.SetTestTimeout(v) 286 | if err != nil { 287 | log.Error(err) 288 | return 289 | } 290 | } 291 | log.Info("外网测试超时时间 (秒): ", setting.TestTimeout()) 292 | }, 293 | }) 294 | baseSettingCmd.AddCmd(&ishell.Cmd{ 295 | Name: "test.mintime", 296 | Func: func(c *ishell.Context) { 297 | if len(c.Args) > 0 { 298 | v, err := strconv.Atoi(c.Args[0]) 299 | if err != nil { 300 | log.Error("非法输入") 301 | return 302 | } 303 | err = setting.SetTestMinTime(v) 304 | if err != nil { 305 | log.Error(err) 306 | return 307 | } 308 | } 309 | log.Info("批量测试终止时间 (毫秒): ", setting.TestMinTime()) 310 | }, 311 | }) 312 | baseSettingCmd.AddCmd(&ishell.Cmd{ 313 | Name: "test.url", 314 | Func: func(c *ishell.Context) { 315 | if len(c.Args) > 0 { 316 | err := setting.SetTestUrl(c.Args[0]) 317 | if err != nil { 318 | log.Warn(err) 319 | return 320 | } 321 | } 322 | log.Info("外网测试URL: ", setting.TestUrl()) 323 | }, 324 | }) 325 | baseSettingCmd.AddCmd(&ishell.Cmd{ 326 | Name: "run_before", 327 | Func: func(c *ishell.Context) { 328 | argMap := FlagsParse(c.Args, map[string]string{ 329 | "c": "close", 330 | }) 331 | if _, ok := argMap["close"]; ok { 332 | err := setting.SetRunBefore("") 333 | if err != nil { 334 | log.Warn(err) 335 | return 336 | } 337 | } else if _, ok := argMap["data"]; ok { 338 | err := setting.SetRunBefore(argMap["data"]) 339 | if err != nil { 340 | log.Warn(err) 341 | return 342 | } 343 | } 344 | log.Info("启动时执行: ", setting.RunBefore()) 345 | }, 346 | }) 347 | shell.AddCmd(baseSettingCmd) 348 | } 349 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Txray 2 | 3 | Txray 是一款 xray 终端版客户端,使用 go 编写。 4 | 5 | 项目地址:https://github.com/hsernos/Txray 6 | 7 | Project X core: https://github.com/XTLS/Xray-core 8 | 9 | ## 注意 10 | 11 | 此文档只针对于最新的 commit, 可能不适用于已发布的最新版本. 12 | 13 | ## 特色 14 | 15 | 1.多平台支持, 支持 Windows, macOS, linux. 16 | 17 | 2.Tab 键命令补齐 18 | 19 | 3.支持 VMess、Shadowsocks、Trojan、VLESS、VMessAEAD、Socks 协议 20 | 21 | ## 安装和使用 22 | 23 | 1.下载对应平台架构的[Txray](https://github.com/hsernos/Txray/releases)和[xray](https://github.com/XTLS/Xray-core/releases),按如下目录结构解压放置 24 | 25 | ``` 26 | Txray(目录命名随意) 27 | │ Txray 28 | │ README.md 29 | │ 30 | └───xray-core (目录命名随意) 31 | │ xray 32 | │ geoip.dat 33 | | geosite.dat 34 | │ ... 35 | 36 | ``` 37 | 38 | 2.将 Txray 程序所在目录添加到 PATH 环境变量(添加环境变量请自行谷歌或百度) 39 | 40 | 3.非 Windows 平台用户添加`Txray`和`xray`可执行权限 41 | 42 | ``` 43 | # 进入 Txray 所在目录执行 'chmod u+x Txray' 44 | # 为 Txray 添加可执行权限 45 | [xxx@xxx Txray-linux-64]$ chmod u+x Txray 46 | ``` 47 | 48 | ``` 49 | # 进入xray所在目录执行 'chmod u+x xray' 50 | # 为 xray 添加可执行权限 51 | [xxx@xxx Xray-linux-64]$ chmod u+x xray 52 | ``` 53 | 54 | 4.打开终端输入`Txray`回车进入 Shell 交互 或 继续在末尾添加命令直接运行 55 | 56 | ``` 57 | # 1.shell交互运行,命令可Tab补齐 58 | [xxx@xxx xxxx]$ Txray 59 | 60 | # 2.直接运行,如更新节点 (不会进入shell交互) 61 | [xxx@xxx xxxx]$ Txray sub update-node 62 | ``` 63 | 64 | ## 稍高级使用 65 | 66 | 1.单电脑多系统共用同一份配置文件(配置环境变量`TXRAY_HOME`) 67 | 68 | - Txray 检测 xray-core 所在的优先级: 环境变量 `CORE_HOME` > Txray 所在目录 > 环境变量 `PATH` 中的目录 69 | - Txray 检测 geosite.dat和geoip.dat文件目录 环境变量 `XRAY_LOCATION_ASSET` > xray所在目录 70 | - 配置文件目录优先级: 环境变量 `TXRAY_HOME` > Txray 所在目录 71 | 72 | 2.开机自启,请自行谷歌或百度查找对应系统的开机自启脚本的写法 73 | 74 | PS:开机自启推荐搭配[命令别名](#查看命令别名帮助文档)使用 75 | 76 | 77 | 78 | ## 目录 79 | 80 | - [编译/交叉编译 说明](#编译交叉编译-说明) 81 | - [命令列表及说明 (以下全为Shell交互下的演示说明)](#命令列表及说明) 82 | - [命令总览](#命令总览) 83 | - [查看基本设置帮助文档](#查看基本设置帮助文档) 84 | - [查看基本设置](#查看基本设置) 85 | - [修改基本设置](#修改基本设置) 86 | - [查看订阅帮助文档](#查看订阅帮助文档) 87 | - [添加订阅](#添加订阅) 88 | - [查看订阅](#查看订阅) 89 | - [修改订阅](#修改订阅) 90 | - [删除订阅](#删除订阅) 91 | - [从订阅更新节点](#从订阅更新节点) 92 | - [查看节点帮助文档](#查看节点帮助文档) 93 | - [添加节点](#添加节点) 94 | - [查看节点](#查看节点) 95 | - [删除节点](#删除节点) 96 | - [tcping 测试](#tcping测试) 97 | - [节点查找](#节点查找) 98 | - [导出节点](#导出节点) 99 | - [节点排序](#节点排序) 100 | - [查看节点过滤器帮助文档](#查看节点过滤器帮助文档) 101 | - [添加过滤器规则](#添加过滤器规则) 102 | - [过滤节点](#过滤节点) 103 | - [查看节点回收站帮助文档](#查看节点回收站帮助文档) 104 | - [查看命令别名帮助文档](#查看命令别名帮助文档) 105 | - [添加和修改别名](#添加和修改别名) 106 | - [查看路由帮助文档](#查看路由帮助文档) 107 | - [添加路由](#添加路由) 108 | - [domain 路由规则](#domain路由规则) 109 | - [ip 路由规则](#ip路由规则) 110 | - [启动或重启 xray-core 服务](#启动或重启xray-core服务) 111 | - [停止 xray-core 服务](#停止xray-core服务) 112 | - [显示运行时 xray-core 的日志](#显示运行时xray-core的日志) 113 | - [已知问题](#已知问题) 114 | - [交流反馈](#交流反馈) 115 | 116 | 117 | 118 | # 编译/交叉编译 说明 119 | 120 | 1. 在终端下进入项目目录 121 | 122 | 2. 设置`GOPROXY`,提高编译所需依赖的下载速度 123 | Linux/Mac 下,运行 `GOPROXY=https://goproxy.cn,direct` 124 | Windows 下,运行 `set GOPROXY=https://goproxy.cn,direct` 125 | 126 | 3. 编译常用平台 127 | 运行 `go build Txray.go`, 可编译当前平台的版本 128 | 运行 `python3 build.py`, 可编译常用平台的版本 129 | 130 | 4. 编译其他平台 131 | 运行 `go tool dist list` 查看所有支持的 GOOS/GOARCH 132 | 133 | Linux/Darwin 例子: 编译 Windows 下的 64 位程序 134 | 135 | `GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build Txray.go` 136 | 137 | Windows 例子: 编译 Linux 下的 32 位程序 138 | 139 | `set GOOS=linux` 140 | `set GOARCH=386` 141 | `set CGO_ENABLED=0` 142 | `go build Txray.go` 143 | 144 | # 命令列表及说明 145 | 146 | > 在终端中运行 Txray 进入 shell 交互 147 | 148 | ## 命令总览 149 | 150 | ``` 151 | Commands: 152 | setting 基础设置 使用 'setting help' 查看详细用法 153 | node 节点管理 使用 'node help' 查看详细用法 154 | sub 订阅管理 使用 'sub help' 查看详细用法 155 | routing 路由管理 使用 'routing help' 查看详细用法 156 | filter 节点过滤 使用 'filter help' 查看详细用法 157 | recycle 回收站 使用 'recycle help' 查看详细用法 158 | alias 命令别名 使用 'alias help' 查看详细用法 159 | help, -h 查看帮助信息 160 | version, -v 查看版本 161 | clear 清屏 162 | exit 退出程序 163 | run 启动或重启节点 164 | stop 关闭节点 165 | log 查看运行时xray日志 166 | 167 | Usage: run [索引式] 168 | run [索引式] 默认为上一次运行节点,如果选中多个节点,则选择访问 'setting' 中测试国外URL延迟最小的 169 | 170 | 171 | 说明: 172 | 一、索引式:更简单的批量选择 173 | 1.选择前6个:'1,2,3,4,5,6' 或 '1-3,4-6' 或 '1-6' 或 '-6' 174 | 2.选择第6个及后面的所有:'6-' 175 | 3.选择第6个:'6' 176 | 4.选择所有:'all' 或 '-' 177 | 注意:超出部分会被忽略,'all' 只能单独使用 178 | 179 | 二、[] 和 {}:帮助说明中的中括号和大括号 180 | 1. []: 表示该选项可忽略 181 | 2. {}: 表示该选项为必须,不可忽略 182 | ``` 183 | 184 | ## 查看基本设置帮助文档 185 | 186 | ``` 187 | >>> setting help 188 | setting {commands} 189 | 190 | Commands: 191 | 查看所有设置 192 | help 查看帮助 193 | 194 | socks [port] 设置socks端口 195 | http [port] 设置http端口, 0为关闭http监听 196 | udp [y|n] 是否启用udp转发 197 | sniffing [y|n] 是否启用流量地址监听 198 | from_lan_conn [y|n] 是否启用来自局域网连接 199 | mux [y|n] 是否启用多路复用(下载和看视频时建议关闭) 200 | 201 | dns.port [port] 设置DNS端口 202 | dns.foreign [dns] 设置国外DNS 203 | dns.domestic [dns] 设置国内DNS 204 | dns.backup [dns] 设置国内备用DNS 205 | 206 | routing.strategy {1|2|3} 设置路由策略为{AsIs|IPIfNonMatch|IPOnDemand} 207 | routing.bypass {y|n} 是否绕过局域网及大陆 208 | 209 | test.url [url] 设置外网测试URL 210 | test.timeout [time] 设置外网测试超时时间 (秒) 211 | test.mintime [time] 设置批量测试终止时间 (毫秒) 212 | 213 | run_before [命令组] [flags] 程序启动时执行命令或命令组,可与命令别名搭配 214 | 215 | 216 | run_before Flags 217 | -c, --close 启动时不执行任何命令 218 | 219 | 说明: 220 | 1.命令,如 'node' 'node tcping' 'sub update-node' 这样的单条命令。 221 | 2.命令组,形如 'sub update-node | node tcping | run' 这样的多条命令,以 '|' 分隔,顺序执行。 222 | PS:命令组包含命令,即命令组也可以设置单条命令 223 | ``` 224 | 225 | ### 查看基本设置 226 | 227 | ``` 228 | >>> setting 229 | +-----------+----------+---------+--------------+--------------------+----------+ 230 | | SOCKS端口 | HTTP端口 | UDP转发 | 流量地址监听 | 允许来自局域网连接 | 多路复用 | 231 | +-----------+----------+---------+--------------+--------------------+----------+ 232 | | 1080 | 0 | true | true | true | true | 233 | +-----------+----------+---------+--------------+--------------------+----------+ 234 | +---------+---------+--------------+-----------------+------------+------------------+ 235 | | DNS端口 | 国外DNS | 国内DNS | 备用国内DNS | 路由策略 | 绕过局域网和大陆 | 236 | +---------+---------+--------------+-----------------+------------+------------------+ 237 | | 1351 | 1.1.1.1 | 119.29.29.29 | 114.114.114.114 | IPOnDemand | true | 238 | +---------+---------+--------------+-----------------+------------+------------------+ 239 | +-------------------------+-------------------+-------------------------+------------+ 240 | | 测试国外URL | 测试超时时间 (秒) | 批量测试终止时间 (毫秒) | 启动时执行 | 241 | +-------------------------+-------------------+-------------------------+------------+ 242 | | https://www.youtube.com | 6 | 1000 | | 243 | +-------------------------+-------------------+-------------------------+------------+ 244 | ``` 245 | 246 | ### 修改基本设置 247 | 248 | ``` 249 | # 修改socks监听端口为3333 250 | >>> setting socks 3333 251 | 252 | # 修改http监听端口为3334 253 | >>> setting http 3334 254 | 255 | # 修改不绕过局域网和大陆 256 | >>> setting routing.bypass n 257 | 258 | # 修改路由策略为IPIfNonMatch, {1|2|3}=>{AsIs|IPIfNonMatch|IPOnDemand} 259 | >>> setting routing.strategy 2 260 | 261 | # 启动时运行仅针对进入shell交互才会触发 262 | # 设置启动时从订阅更新节点 263 | >>> setting run_before "sub update-node" 264 | 265 | # 设置启动时对节点进行tcp测试,然后运行延迟最小的那个 266 | >>> setting run_before "node tcping | run" 267 | 268 | # 设置启动时不执行任何命令 269 | >>> setting run_before -c 270 | 271 | # 设置批量测试终止时间为1000,即节点测试延迟在1~1000ms内就会停止,不会继续测试后续节点 272 | >>> setting test.mintime 1000 273 | ``` 274 | 275 | ## 查看订阅帮助文档 276 | 277 | ``` 278 | >>> sub help 279 | sub {commands} [flags] ... 280 | 281 | Commands: 282 | 查看订阅信息 283 | help 查看帮助 284 | rm {索引式} 删除订阅 285 | add {订阅url} [flags] 添加订阅 286 | mv {索引式} {flags} 修改订阅 287 | update-node [索引式] [flags] 从订阅更新节点, 索引式会忽略是否启用 288 | 289 | add Flags 290 | -r, --remarks {别名} 定义别名 291 | 292 | mv Flags 293 | -u, --url {订阅url} 修改订阅链接 294 | -r, --remarks {别名} 定义别名 295 | --using {y|n} 是否启用此订阅 296 | 297 | update-node Flags 298 | -s, --socks5 [port] 通过本地的socks5代理更新, 默认为设置中的socks5端口 299 | -h, --http [port] 通过本地的http代理更新, 默认为设置中的http端口 300 | -a, --addr {address} 对上面两个参数的补充, 修改代理地址 301 | ``` 302 | 303 | ### 添加订阅 304 | 305 | ``` 306 | # 添加订阅链接为https://sublink.com 307 | >>> sub add https://sublink.com 308 | 309 | # 添加订阅链接为https://sublink.com,并命名为test 310 | >>> sub add https://sublink.com -r test 311 | ``` 312 | 313 | ### 查看订阅 314 | 315 | ``` 316 | # 查看全部订阅 317 | >>> sub 318 | +------+-------+---------------------+----------+ 319 | | 索引 | 别名 | URL | 是否启用 | 320 | +------+-------+---------------------+----------+ 321 | | 1 | test1 | https://sublink.com | true | 322 | | 2 | test2 | https://sublink.com | true | 323 | | 3 | test3 | https://sublink.com | true | 324 | | 4 | test4 | https://sublink.com | true | 325 | | 5 | test5 | https://sublink.com | true | 326 | | 6 | test6 | https://sublink.com | true | 327 | +------+-------+---------------------+----------+ 328 | 329 | # 查看索引为2,3,4的订阅 330 | >>> sub 2-4 331 | +------+-------+---------------------+----------+ 332 | | 索引 | 别名 | URL | 是否启用 | 333 | +------+-------+---------------------+----------+ 334 | | 2 | test2 | https://sublink.com | true | 335 | | 3 | test3 | https://sublink.com | true | 336 | | 4 | test4 | https://sublink.com | true | 337 | +------+-------+---------------------+----------+ 338 | ``` 339 | 340 | ### 修改订阅 341 | 342 | ``` 343 | # 修改索引为1的订阅链接为https://test.com,别名为test8 344 | >>> sub mv 1 -u https://test.com -r test8 345 | >>> sub 1 346 | +------+-------+------------------+----------+ 347 | | 索引 | 别名 | URL | 是否启用 | 348 | +------+-------+------------------+----------+ 349 | | 1 | test8 | https://test.com | true | 350 | +------+-------+------------------+----------+ 351 | 352 | # 禁用索引为3和5的订阅链接 353 | >>> sub mv 3,5 --using n 354 | >>> sub 355 | +------+-------+---------------------+----------+ 356 | | 索引 | 别名 | URL | 是否启用 | 357 | +------+-------+---------------------+----------+ 358 | | 1 | test8 | https://sublink.com | true | 359 | | 2 | test2 | https://sublink.com | true | 360 | | 3 | test3 | https://sublink.com | false | 361 | | 4 | test4 | https://sublink.com | true | 362 | | 5 | test5 | https://sublink.com | false | 363 | | 6 | test6 | https://sublink.com | true | 364 | +------+-------+---------------------+----------+ 365 | ``` 366 | 367 | ### 删除订阅 368 | 369 | ``` 370 | # 删除索引为3和5的订阅 371 | >>> sub rm 3,5 372 | 373 | # 删除所有订阅 374 | >>> sub rm all 375 | ``` 376 | 377 | ### 从订阅更新节点 378 | 379 | ``` 380 | # 从启用的订阅且不使用代理更新节点 381 | >>> sub update-node 382 | 383 | # 从索引范围更新节点,无论是否启用 384 | >>> sub update-node 1,3,6 385 | 386 | # 使用端口为2333的本地socks5代理更新节点 387 | >>> sub update-node -s 2333 388 | 389 | # 使用设置中的socks端口通过本地socks5代理更新节点 390 | >>> sub update-node -s 391 | 392 | # 使用端口为2334的本地http代理更新节点 393 | >>> sub update-node -h 2334 394 | 395 | # 使用端口为2333,地址为1.2.3.4的socks代理更新节点 396 | >>> sub update-node -s 2333 -a 1.2.3.4 397 | ``` 398 | 399 | ## 查看节点帮助文档 400 | 401 | ``` 402 | >>> node help 403 | node {commands} [flags] ... 404 | 405 | Commands: 406 | [索引式] [flags] 查看节点信息, 默认 'all' 407 | help 查看帮助 408 | tcping 测试节点tcp延迟 409 | sort {0|1|2|3|4|5} 排序方式,分别按{逆转|协议|别名|地址|端口|测试结果}排序 410 | info {索引} 查看单个节点详细信息 411 | rm {索引式} 删除节点 412 | find {关键词} 查找节点(按别名) 413 | add [flags] 添加节点 414 | export [索引式] [flags] 导出节点链接, 默认'all' 415 | 416 | Flags 417 | -d, --desc 降序查看 418 | 419 | add Flags 420 | -l, --link {link} 从链接导入一条节点 421 | -f, --file {path} 从节点链接文件或订阅文件导入节点 422 | -c, --clipboard 从剪贴板读取的节点链接或订阅文本导入节点 423 | 424 | export Flags 425 | -c, --clipboard 导出节点链接到剪贴板 426 | ``` 427 | 428 | ### 添加节点 429 | 430 | ``` 431 | # 添加一个vmess节点 432 | >>> node add -l vmess://xxxxxxXXXXxxxxxXX 433 | 434 | # 添加一个trojan节点 435 | >>> node add -l trojan://xxxxxxXXXXxxxxxXX 436 | 437 | # 由链接文件批量添加节点 438 | >>> node add -f /home/links.txt 439 | 440 | # 解析订阅文件添加节点,可以将订阅文件下载下来然后从本地导入 441 | >>> node add -f /home/subtext.txt 442 | 443 | # 从剪贴板读取的节点链接或订阅文本导入节点, 功效和上面从文件导入一样 444 | >>> node add -c 445 | 446 | # 手动添加一个节点 447 | >>> node add 448 | ``` 449 | 450 | ### 查看节点 451 | 452 | ``` 453 | # 查看前20个节点 454 | >>> node 1-20 455 | 456 | # 降序查看所有节点 457 | >>> node -d 458 | 459 | # 查看某个节点的全部信息 460 | >>> node info 1 461 | 462 | ``` 463 | 464 | ### 删除节点 465 | 466 | ``` 467 | # 删除前20个节点 468 | >>> node rm 1-20 469 | ``` 470 | 471 | ### tcping 测试 472 | 473 | ``` 474 | # tcping测试所有节点 475 | >>> node tcping 476 | ``` 477 | 478 | ### 节点查找 479 | 480 | ``` 481 | # 查找关键词为'vip'的节点 482 | >>> node find vip 483 | 484 | # 查找关键词为'香港'的节点 485 | >>> node find "香港" 486 | ``` 487 | 488 | ### 导出节点 489 | 490 | ``` 491 | # 导出前20个节点到终端 492 | >>> node export -20 493 | 494 | # 导出前20个节点到剪贴板 495 | >>> node export -20 -c 496 | ``` 497 | 498 | ### 节点排序 499 | 500 | ``` 501 | # 逆转节点顺序 502 | >>> node sort 0 503 | 504 | # 按别名排序 505 | >>> node sort 2 506 | ``` 507 | 508 | ## 查看节点过滤器帮助文档 509 | 510 | > 过滤器会在添加节点的时候自动运行,也可以使用 'filter run' 手动运行 511 | 512 | ``` 513 | >>> filter help 514 | filter {commands} ... 515 | 516 | Commands: 517 | 查看过滤规则 518 | help 查看帮助 519 | rm {索引式} 删除过滤规则 520 | open {索引式} 开启过滤规则 521 | close {索引式} 关闭过滤规则 522 | add {过滤规则} 添加过滤规则 523 | run [过滤规则] 手动过滤节点,默认使用内置规则 524 | 525 | PS: 过滤规则==> '过滤范围:正则表达式' 526 | 过滤范围可选值 proto:|name:|addr:|port: 分别代表 协议|别名|地址|端口 527 | 默认为 'name:' 528 | ``` 529 | 530 | ### 添加过滤器规则 531 | 532 | ``` 533 | # 添加地址为baidu.com的过滤规则 534 | >>> filter add addr:baidu.com 535 | 536 | # 添加协议为VMess的过滤规则 537 | >>> filter add proto:VMess 538 | 539 | # 添加别名含有'美国'的过滤规则 540 | >>> filter add "美国" 541 | ``` 542 | 543 | ### 过滤节点 544 | 545 | ``` 546 | # 删除地址为baidu.com的节点 547 | >>> filter run addr:baidu.com 548 | 549 | # 运行已有规则 550 | >>> filter run 551 | ``` 552 | 553 | ## 查看节点回收站帮助文档 554 | 555 | > 数据仅保存当次交互中 556 | 557 | ``` 558 | >>> recycle help 559 | recycle {commands} ... 560 | 561 | Commands: 562 | {索引式} 查看节点回收站 563 | help 查看帮助 564 | restore {索引式} 恢复节点 565 | clear 清空节点回收站 566 | 567 | PS: 回收站的数据仅运行存在 (仅存储在内存中) 568 | ``` 569 | 570 | ## 查看命令别名帮助文档 571 | 572 | > 不能覆盖自带命令,小心使用,不要弄成死循环了 573 | 574 | ``` 575 | >>> alias help 576 | alias {commands} ... 577 | 578 | Commands: 579 | 查看命令别名 580 | help 查看帮助 581 | set {别名} {命令组} 开启过滤规则 582 | rm {索引式} 删除命令别名 583 | 584 | 说明: 585 | 1.命令,如 'node' 'node tcping' 'sub update-node' 这样的单条命令。 586 | 2.命令组,形如 'sub update-node | node tcping | run' 这样的多条命令,以 '|' 分隔,顺序执行。 587 | PS:命令组包含命令,即命令组也可以设置单条命令 588 | ``` 589 | 590 | ### 添加和修改别名 591 | 592 | ``` 593 | # 设置别名 'one' 为更新订阅,然后tcping,最后运行延迟最小的那个 594 | >>> alias set one "sub update-node | node tcping | run" 595 | 596 | # 运行 597 | >>> one 598 | 599 | # 等价于 'sub update-node | node tcping | run 0-10' 600 | >>> one 0-10 601 | ``` 602 | 603 | ## 查看路由帮助文档 604 | 605 | ``` 606 | >>> route help 607 | routing {commands} [flags] ... 608 | 609 | Commands: 610 | help 查看帮助 611 | block [索引式] | [flags] 查看或管理禁止路由规则 612 | direct [索引式] | [flags] 查看或管理直连路由规则 613 | proxy [索引式] | [flags] 查看或管理代理路由规则 614 | 615 | block, direct, proxy Flags 616 | -a, --add {规则} 添加路由规则 617 | -r, --rm {索引式} 删除路由规则 618 | -f, --file {path} 从文件导入规则 619 | -c, --clipboard 从剪贴板导入规则 620 | 621 | PS: 规则详情请访问 https://xtls.github.io/config/routing.html#ruleobject 622 | ``` 623 | 624 | ### 添加路由 625 | 626 | ``` 627 | # 添加www.baidu.com到黑名单 628 | >>> routing block -a www.baidu.com 629 | 630 | # 添加www.google.com到代理名单 631 | >>> routing proxy -a www.google.com 632 | 633 | # 从文件批量导入到黑名单 634 | >>> routing block -f /home/xxx/block.txt 635 | 636 | # 从剪贴板导入到黑名单 637 | >>> routing block -c 638 | 639 | ``` 640 | 641 | ### domain路由规则 642 | 643 | - 纯字符串: 当此字符串匹配目标域名中任意部分,该规则生效。比如`sina.com`可以匹配`sina.com`、sina.com.cn 和www.sina.com,但不匹配`sina.cn`。 644 | - 正则表达式: 由`regexp:`开始,余下部分是一个正则表达式。当此正则表达式匹配目标域名时,该规则生效。例如`regexp:\\.goo.*\\.com$`匹配`www.google.com`、`fonts.googleapis.com`,但不匹配`google.com`。 645 | - 子域名 (推荐): 由`domain:`开始,余下部分是一个域名。当此域名是目标域名或其子域名时,该规则生效。例如`domain:xray.com`匹配`www.xray.com`、`xray.com`,但不匹配`xxray.com`。 646 | - 完整匹配: 由`full:`开始,余下部分是一个域名。当此域名完整匹配目标域名时,该规则生效。例如`full:xray.com`匹配`xray.com`但不匹配`www.xray.com`。 647 | - 预定义域名列表:由`"geosite:"`开头,余下部分是一个名称,如`geosite:google`或者`geosite:cn`。名称及域名列表参考[预定义域名列表](https://www.v2ray.com/chapter_02/03_routing.html#dlc)。 648 | - 从文件中加载域名: 形如`ext:file:tag`,必须以`ext:`(小写)开头,后面跟文件名和标签,文件存放在[资源目录](https://www.v2ray.com/chapter_02/env.html#asset-location)中,文件格式与`geosite.dat`相同,标签必须在文件中存在。 649 | 650 | ### ip路由规则 651 | 652 | - IP: 形如`127.0.0.1`。 653 | - [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing): 形如`10.0.0.0/8`. 654 | - GeoIP: 形如`geoip:cn`,必须以`geoip:`(小写)开头,后面跟双字符国家代码,支持几乎所有可以上网的国家。 655 | - 特殊值:`geoip:private` (xray 3.5+),包含所有私有地址,如`127.0.0.1`。 656 | - 从文件中加载 IP: 形如`ext:file:tag`,必须以`ext:`(小写)开头,后面跟文件名和标签,文件存放在[资源目录](https://www.v2ray.com/chapter_02/env.html#asset-location)中,文件格式与`geoip.dat`相同标签必须在文件中存在。 657 | 658 | ## 启动或重启xray-core服务 659 | 660 | ``` 661 | # 启动或重启索引为3的节点 662 | >>> run 3 663 | 664 | # 自动选择所有节点中访问YouTube延迟最小的那个节点 665 | >>> run all 666 | 667 | # 自动选择1-10中访问YouTube延迟最小的那个节点 668 | >>> run 1-10 669 | 670 | # 自动选择tcp延迟最小的10个中访问YouTube延迟最小的那个节点 671 | >>> run -t -10 672 | ``` 673 | 674 | ## 停止xray-core服务 675 | 676 | ``` 677 | # 停止上次启动的xray-core进程 678 | >>> stop 679 | 680 | ``` 681 | 682 | ## 显示运行时xray-core的日志 683 | 684 | ``` 685 | # 显示运行时 xray-core 的日志 686 | >>> log 687 | 688 | ``` 689 | 690 | # 已知问题 691 | 692 | - 有时直接从订阅更新节点失败,可以用浏览器下载订阅文本然后使用 'node add -f {绝对路径}' 导入,或者使用代理导入(sub update-node -s [端口]) 693 | 694 | # 交流反馈 695 | 696 | 提交 Issue: [Issues](https://github.com/hsernos/Txray/issues) 697 | -------------------------------------------------------------------------------- /xray/config.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "Txray/core" 5 | "Txray/core/protocols" 6 | "Txray/core/protocols/field" 7 | "Txray/core/routing" 8 | "Txray/core/setting" 9 | "Txray/log" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | // 生成xray-core配置文件 15 | func GenConfig(node protocols.Protocol) string { 16 | path := filepath.Join(core.GetConfigDir(), "config.json") 17 | var conf = map[string]interface{}{ 18 | "log": logConfig(), 19 | "inbounds": inboundsConfig(), 20 | "outbounds": outboundConfig(node), 21 | "policy": policyConfig(), 22 | "dns": dnsConfig(), 23 | "routing": routingConfig(), 24 | } 25 | err := core.WriteJSON(conf, path) 26 | if err != nil { 27 | log.Error(err) 28 | panic(err) 29 | } 30 | return path 31 | } 32 | 33 | // 日志 34 | func logConfig() interface{} { 35 | path := core.LogFile 36 | return map[string]string{ 37 | "access": path, 38 | "loglevel": "warning", 39 | } 40 | } 41 | 42 | // 入站 43 | func inboundsConfig() interface{} { 44 | listen := "127.0.0.1" 45 | if setting.FromLanConn() { 46 | listen = "0.0.0.0" 47 | } 48 | data := []interface{}{ 49 | map[string]interface{}{ 50 | "tag": "proxy", 51 | "port": setting.Socks(), 52 | "listen": listen, 53 | "protocol": "socks", 54 | "sniffing": map[string]interface{}{ 55 | "enabled": setting.Sniffing(), 56 | "destOverride": []string{ 57 | "http", 58 | "tls", 59 | }, 60 | }, 61 | "settings": map[string]interface{}{ 62 | "auth": "noauth", 63 | "udp": setting.UDP(), 64 | "userLevel": 0, 65 | }, 66 | }, 67 | } 68 | if setting.Http() > 0 { 69 | data = append(data, map[string]interface{}{ 70 | "tag": "http", 71 | "port": setting.Http(), 72 | "listen": listen, 73 | "protocol": "http", 74 | "settings": map[string]interface{}{ 75 | "userLevel": 0, 76 | }, 77 | }) 78 | } 79 | if setting.DNSPort() > 0 { 80 | data = append(data, map[string]interface{}{ 81 | "tag": "dns-in", 82 | "port": setting.DNSPort(), 83 | "listen": listen, 84 | "protocol": "dokodemo-door", 85 | "settings": map[string]interface{}{ 86 | "userLevel": 0, 87 | "address": setting.DNSForeign(), 88 | "network": "tcp,udp", 89 | "port": 53, 90 | }, 91 | }) 92 | } 93 | return data 94 | } 95 | 96 | // 本地策略 97 | func policyConfig() interface{} { 98 | return map[string]interface{}{ 99 | "levels": map[string]interface{}{ 100 | "0": map[string]interface{}{ 101 | "handshake": 4, 102 | "connIdle": 300, 103 | "uplinkOnly": 1, 104 | "downlinkOnly": 1, 105 | "bufferSize": 10240, 106 | }, 107 | }, 108 | "system": map[string]interface{}{ 109 | "statsInboundUplink": true, 110 | "statsInboundDownlink": true, 111 | }, 112 | } 113 | } 114 | 115 | // DNS 116 | func dnsConfig() interface{} { 117 | servers := make([]interface{}, 0) 118 | if setting.DNSDomestic() != "" { 119 | servers = append(servers, map[string]interface{}{ 120 | "address": setting.DNSDomestic(), 121 | "port": 53, 122 | "domains": []interface{}{ 123 | "geosite:cn", 124 | }, 125 | "expectIPs": []interface{}{ 126 | "geoip:cn", 127 | }, 128 | }) 129 | } 130 | if setting.DNSBackup() != "" { 131 | servers = append(servers, map[string]interface{}{ 132 | "address": setting.DNSBackup(), 133 | "port": 53, 134 | "domains": []interface{}{ 135 | "geosite:cn", 136 | }, 137 | "expectIPs": []interface{}{ 138 | "geoip:cn", 139 | }, 140 | }) 141 | } 142 | if setting.DNSForeign() != "" { 143 | servers = append(servers, map[string]interface{}{ 144 | "address": setting.DNSForeign(), 145 | "port": 53, 146 | "domains": []interface{}{ 147 | "geosite:geolocation-!cn", 148 | "geosite:speedtest", 149 | }, 150 | }) 151 | } 152 | return map[string]interface{}{ 153 | "hosts": map[string]interface{}{ 154 | "domain:googleapis.cn": "googleapis.com", 155 | }, 156 | "servers": servers, 157 | } 158 | } 159 | 160 | // 路由 161 | func routingConfig() interface{} { 162 | rules := make([]interface{}, 0) 163 | if setting.DNSPort() != 0 { 164 | rules = append(rules, map[string]interface{}{ 165 | "type": "field", 166 | "inboundTag": []interface{}{ 167 | "dns-in", 168 | }, 169 | "outboundTag": "dns-out", 170 | }) 171 | } 172 | if setting.DNSForeign() != "" { 173 | rules = append(rules, map[string]interface{}{ 174 | "type": "field", 175 | "port": 53, 176 | "outboundTag": "proxy", 177 | "ip": []string{ 178 | setting.DNSForeign(), 179 | }, 180 | }) 181 | } 182 | if setting.DNSDomestic() != "" || setting.DNSBackup() != "" { 183 | var ip []string 184 | if setting.DNSDomestic() != "" { 185 | ip = append(ip, setting.DNSDomestic()) 186 | } 187 | if setting.DNSBackup() != "" { 188 | ip = append(ip, setting.DNSBackup()) 189 | } 190 | rules = append(rules, map[string]interface{}{ 191 | "type": "field", 192 | "port": 53, 193 | "outboundTag": "direct", 194 | "ip": ip, 195 | }) 196 | } 197 | ips, domains := routing.GetRulesGroupData(routing.TypeBlock) 198 | if len(ips) != 0 { 199 | rules = append(rules, map[string]interface{}{ 200 | "type": "field", 201 | "outboundTag": "block", 202 | "ip": ips, 203 | }) 204 | } 205 | if len(domains) != 0 { 206 | rules = append(rules, map[string]interface{}{ 207 | "type": "field", 208 | "outboundTag": "block", 209 | "domain": domains, 210 | }) 211 | } 212 | ips, domains = routing.GetRulesGroupData(routing.TypeDirect) 213 | if len(ips) != 0 { 214 | rules = append(rules, map[string]interface{}{ 215 | "type": "field", 216 | "outboundTag": "direct", 217 | "ip": ips, 218 | }) 219 | } 220 | if len(domains) != 0 { 221 | rules = append(rules, map[string]interface{}{ 222 | "type": "field", 223 | "outboundTag": "direct", 224 | "domain": domains, 225 | }) 226 | } 227 | ips, domains = routing.GetRulesGroupData(routing.TypeProxy) 228 | if len(ips) != 0 { 229 | rules = append(rules, map[string]interface{}{ 230 | "type": "field", 231 | "outboundTag": "proxy", 232 | "ip": ips, 233 | }) 234 | } 235 | if len(domains) != 0 { 236 | rules = append(rules, map[string]interface{}{ 237 | "type": "field", 238 | "outboundTag": "proxy", 239 | "domain": domains, 240 | }) 241 | } 242 | 243 | if setting.RoutingBypass() { 244 | rules = append(rules, map[string]interface{}{ 245 | "type": "field", 246 | "outboundTag": "direct", 247 | "ip": []string{ 248 | "geoip:private", 249 | "geoip:cn", 250 | }, 251 | }) 252 | rules = append(rules, map[string]interface{}{ 253 | "type": "field", 254 | "outboundTag": "direct", 255 | "domain": []string{ 256 | "geosite:cn", 257 | }, 258 | }) 259 | } 260 | return map[string]interface{}{ 261 | "domainStrategy": setting.RoutingStrategy(), 262 | "rules": rules, 263 | } 264 | } 265 | 266 | // 出站 267 | func outboundConfig(n protocols.Protocol) interface{} { 268 | out := make([]interface{}, 0) 269 | switch n.GetProtocolMode() { 270 | case protocols.ModeTrojan: 271 | t := n.(*protocols.Trojan) 272 | out = append(out, trojanOutbound(t)) 273 | case protocols.ModeShadowSocks: 274 | ss := n.(*protocols.ShadowSocks) 275 | out = append(out, shadowsocksOutbound(ss)) 276 | case protocols.ModeVMess: 277 | v := n.(*protocols.VMess) 278 | out = append(out, vMessOutbound(v)) 279 | case protocols.ModeSocks: 280 | v := n.(*protocols.Socks) 281 | out = append(out, socksOutbound(v)) 282 | case protocols.ModeVLESS: 283 | v := n.(*protocols.VLess) 284 | out = append(out, vLessOutbound(v)) 285 | case protocols.ModeVMessAEAD: 286 | v := n.(*protocols.VMessAEAD) 287 | out = append(out, vMessAEADOutbound(v)) 288 | } 289 | out = append(out, map[string]interface{}{ 290 | "tag": "direct", 291 | "protocol": "freedom", 292 | "settings": map[string]interface{}{}, 293 | }) 294 | out = append(out, map[string]interface{}{ 295 | "tag": "block", 296 | "protocol": "blackhole", 297 | "settings": map[string]interface{}{ 298 | "response": map[string]interface{}{ 299 | "type": "http", 300 | }, 301 | }, 302 | }) 303 | out = append(out, map[string]interface{}{ 304 | "tag": "dns-out", 305 | "protocol": "dns", 306 | }) 307 | return out 308 | } 309 | 310 | //Shadowsocks 311 | func shadowsocksOutbound(ss *protocols.ShadowSocks) interface{} { 312 | return map[string]interface{}{ 313 | "tag": "proxy", 314 | "protocol": "shadowsocks", 315 | "settings": map[string]interface{}{ 316 | "servers": []interface{}{ 317 | map[string]interface{}{ 318 | "address": ss.Address, 319 | "port": ss.Port, 320 | "password": ss.Password, 321 | "method": ss.Method, 322 | "level": 0, 323 | }, 324 | }, 325 | }, 326 | "streamSettings": map[string]interface{}{ 327 | "network": "tcp", 328 | }, 329 | } 330 | } 331 | 332 | // Trojan 333 | func trojanOutbound(trojan *protocols.Trojan) interface{} { 334 | streamSettings := map[string]interface{}{ 335 | "network": "tcp", 336 | "security": "tls", 337 | } 338 | if trojan.Sni() != "" { 339 | streamSettings["tlsSettings"] = map[string]interface{}{ 340 | "allowInsecure": setting.AllowInsecure(), 341 | "serverName": trojan.Sni(), 342 | } 343 | } 344 | return map[string]interface{}{ 345 | "tag": "proxy", 346 | "protocol": "trojan", 347 | "settings": map[string]interface{}{ 348 | "servers": []interface{}{ 349 | map[string]interface{}{ 350 | "address": trojan.Address, 351 | "port": trojan.Port, 352 | "password": trojan.Password, 353 | "level": 0, 354 | }, 355 | }, 356 | }, 357 | "streamSettings": streamSettings, 358 | } 359 | } 360 | 361 | // VMess 362 | func vMessOutbound(vmess *protocols.VMess) interface{} { 363 | mux := setting.Mux() 364 | streamSettings := map[string]interface{}{ 365 | "network": vmess.Net, 366 | "security": vmess.Tls, 367 | } 368 | if vmess.Tls == "tls" { 369 | tlsSettings := map[string]interface{}{ 370 | "allowInsecure": setting.AllowInsecure(), 371 | } 372 | if vmess.Sni != "" { 373 | tlsSettings["serverName"] = vmess.Sni 374 | } 375 | if vmess.Alpn != "" { 376 | tlsSettings["alpn"] = strings.Split(vmess.Alpn, ",") 377 | } 378 | streamSettings["tlsSettings"] = tlsSettings 379 | } 380 | switch vmess.Net { 381 | case "tcp": 382 | streamSettings["tcpSettings"] = map[string]interface{}{ 383 | "header": map[string]interface{}{ 384 | "type": vmess.Type, 385 | }, 386 | } 387 | case "kcp": 388 | kcpSettings := map[string]interface{}{ 389 | "mtu": 1350, 390 | "tti": 50, 391 | "uplinkCapacity": 12, 392 | "downlinkCapacity": 100, 393 | "congestion": false, 394 | "readBufferSize": 2, 395 | "writeBufferSize": 2, 396 | "header": map[string]interface{}{ 397 | "type": vmess.Type, 398 | }, 399 | } 400 | if vmess.Type != "none" { 401 | kcpSettings["seed"] = vmess.Path 402 | } 403 | streamSettings["kcpSettings"] = kcpSettings 404 | case "ws": 405 | streamSettings["wsSettings"] = map[string]interface{}{ 406 | "path": vmess.Path, 407 | "headers": map[string]interface{}{ 408 | "Host": vmess.Host, 409 | }, 410 | } 411 | case "h2": 412 | mux = false 413 | host := make([]string, 0) 414 | for _, line := range strings.Split(vmess.Host, ",") { 415 | line = strings.TrimSpace(line) 416 | if line != "" { 417 | host = append(host, line) 418 | } 419 | } 420 | streamSettings["httpSettings"] = map[string]interface{}{ 421 | "path": vmess.Path, 422 | "host": host, 423 | } 424 | case "quic": 425 | quicSettings := map[string]interface{}{ 426 | "security": vmess.Host, 427 | "header": map[string]interface{}{ 428 | "type": vmess.Type, 429 | }, 430 | } 431 | if vmess.Host != "none" { 432 | quicSettings["key"] = vmess.Path 433 | } 434 | streamSettings["quicSettings"] = quicSettings 435 | case "grpc": 436 | streamSettings["grpcSettings"] = map[string]interface{}{ 437 | "serviceName": vmess.Path, 438 | "multiMode": vmess.Type == "multi", 439 | } 440 | } 441 | return map[string]interface{}{ 442 | "tag": "proxy", 443 | "protocol": "vmess", 444 | "settings": map[string]interface{}{ 445 | "vnext": []interface{}{ 446 | map[string]interface{}{ 447 | "address": vmess.Add, 448 | "port": vmess.Port, 449 | "users": []interface{}{ 450 | map[string]interface{}{ 451 | "id": vmess.Id, 452 | "alterId": vmess.Aid, 453 | "security": vmess.Scy, 454 | "level": 0, 455 | }, 456 | }, 457 | }, 458 | }, 459 | }, 460 | "streamSettings": streamSettings, 461 | "mux": map[string]interface{}{ 462 | "enabled": mux, 463 | }, 464 | } 465 | } 466 | 467 | // socks 468 | func socksOutbound(socks *protocols.Socks) interface{} { 469 | user := map[string]interface{}{ 470 | "address": socks.Address, 471 | "port": socks.Port, 472 | } 473 | if socks.Username != "" || socks.Password != "" { 474 | user["users"] = []interface{}{ 475 | map[string]interface{}{ 476 | "user": socks.Username, 477 | "pass": socks.Password, 478 | }, 479 | } 480 | } 481 | return map[string]interface{}{ 482 | "tag": "proxy", 483 | "protocol": "socks", 484 | "settings": map[string]interface{}{ 485 | "servers": []interface{}{ 486 | user, 487 | }, 488 | }, 489 | "streamSettings": map[string]interface{}{ 490 | "network": "tcp", 491 | "tcpSettings": map[string]interface{}{ 492 | "header": map[string]interface{}{ 493 | "type": "none", 494 | }, 495 | }, 496 | }, 497 | "mux": map[string]interface{}{ 498 | "enabled": false, 499 | }, 500 | } 501 | } 502 | 503 | // VLESS 504 | func vLessOutbound(vless *protocols.VLess) interface{} { 505 | mux := setting.Mux() 506 | security := vless.GetValue(field.Security) 507 | network := vless.GetValue(field.NetworkType) 508 | user := map[string]interface{}{ 509 | "id": vless.ID, 510 | "flow": vless.GetValue(field.Flow), 511 | "encryption": vless.GetValue(field.VLessEncryption), 512 | "level": 0, 513 | } 514 | streamSettings := map[string]interface{}{ 515 | "network": network, 516 | "security": security, 517 | } 518 | switch security { 519 | case "tls": 520 | tlsSettings := map[string]interface{}{ 521 | "allowInsecure": setting.AllowInsecure(), 522 | } 523 | sni := vless.GetHostValue(field.SNI) 524 | alpn := vless.GetValue(field.Alpn) 525 | if sni != "" { 526 | tlsSettings["serverName"] = sni 527 | } 528 | if alpn != "" { 529 | tlsSettings["alpn"] = strings.Split(alpn, ",") 530 | } 531 | streamSettings["tlsSettings"] = tlsSettings 532 | case "xtls": 533 | xtlsSettings := map[string]interface{}{ 534 | "allowInsecure": setting.AllowInsecure(), 535 | } 536 | sni := vless.GetHostValue(field.SNI) 537 | alpn := vless.GetValue(field.Alpn) 538 | if sni != "" { 539 | xtlsSettings["serverName"] = sni 540 | } 541 | if alpn != "" { 542 | xtlsSettings["alpn"] = strings.Split(alpn, ",") 543 | } 544 | streamSettings["xtlsSettings"] = xtlsSettings 545 | mux = false 546 | case "reality": 547 | realitySettings := map[string]interface{}{ 548 | "show": false, 549 | "fingerprint": vless.GetValue(field.FingerPrint), 550 | "serverName": vless.GetHostValue(field.SNI), 551 | "publicKey": vless.GetValue(field.PublicKey), 552 | "shortId": vless.GetValue(field.ShortId), 553 | "spiderX": vless.GetValue(field.SpiderX), 554 | } 555 | streamSettings["realitySettings"] = realitySettings 556 | mux = false 557 | } 558 | switch network { 559 | case "tcp": 560 | streamSettings["tcpSettings"] = map[string]interface{}{ 561 | "header": map[string]interface{}{ 562 | "type": vless.GetValue(field.TCPHeaderType), 563 | }, 564 | } 565 | case "kcp": 566 | kcpSettings := map[string]interface{}{ 567 | "mtu": 1350, 568 | "tti": 50, 569 | "uplinkCapacity": 12, 570 | "downlinkCapacity": 100, 571 | "congestion": false, 572 | "readBufferSize": 2, 573 | "writeBufferSize": 2, 574 | "header": map[string]interface{}{ 575 | "type": vless.GetValue(field.MkcpHeaderType), 576 | }, 577 | } 578 | if vless.Has(field.Seed.Key) { 579 | kcpSettings["seed"] = vless.GetValue(field.Seed) 580 | } 581 | streamSettings["kcpSettings"] = kcpSettings 582 | case "h2": 583 | mux = false 584 | host := make([]string, 0) 585 | for _, line := range strings.Split(vless.GetHostValue(field.H2Host), ",") { 586 | line = strings.TrimSpace(line) 587 | if line != "" { 588 | host = append(host, line) 589 | } 590 | } 591 | streamSettings["httpSettings"] = map[string]interface{}{ 592 | "path": vless.GetValue(field.H2Path), 593 | "host": host, 594 | } 595 | case "ws": 596 | streamSettings["wsSettings"] = map[string]interface{}{ 597 | "path": vless.GetValue(field.WsPath), 598 | "headers": map[string]interface{}{ 599 | "Host": vless.GetValue(field.WsHost), 600 | }, 601 | } 602 | case "quic": 603 | quicSettings := map[string]interface{}{ 604 | "security": vless.GetValue(field.QuicSecurity), 605 | "header": map[string]interface{}{ 606 | "type": vless.GetValue(field.QuicHeaderType), 607 | }, 608 | } 609 | if vless.GetValue(field.QuicSecurity) != "none" { 610 | quicSettings["key"] = vless.GetValue(field.QuicKey) 611 | } 612 | streamSettings["quicSettings"] = quicSettings 613 | case "grpc": 614 | streamSettings["grpcSettings"] = map[string]interface{}{ 615 | "serviceName": vless.GetValue(field.GrpcServiceName), 616 | "multiMode": vless.GetValue(field.GrpcMode) == "multi", 617 | } 618 | } 619 | return map[string]interface{}{ 620 | "tag": "proxy", 621 | "protocol": "vless", 622 | "settings": map[string]interface{}{ 623 | "vnext": []interface{}{ 624 | map[string]interface{}{ 625 | "address": vless.Address, 626 | "port": vless.Port, 627 | "users": []interface{}{ 628 | user, 629 | }, 630 | }, 631 | }, 632 | }, 633 | "streamSettings": streamSettings, 634 | "mux": map[string]interface{}{ 635 | "enabled": mux, 636 | }, 637 | } 638 | } 639 | 640 | // VMessAEAD 641 | func vMessAEADOutbound(vmess *protocols.VMessAEAD) interface{} { 642 | mux := setting.Mux() 643 | security := vmess.GetValue(field.Security) 644 | network := vmess.GetValue(field.NetworkType) 645 | streamSettings := map[string]interface{}{ 646 | "network": network, 647 | "security": security, 648 | } 649 | switch security { 650 | case "tls": 651 | tlsSettings := map[string]interface{}{ 652 | "allowInsecure": setting.AllowInsecure(), 653 | } 654 | sni := vmess.GetHostValue(field.SNI) 655 | alpn := vmess.GetValue(field.Alpn) 656 | if sni != "" { 657 | tlsSettings["serverName"] = sni 658 | } 659 | if alpn != "" { 660 | tlsSettings["alpn"] = strings.Split(alpn, ",") 661 | } 662 | streamSettings["tlsSettings"] = tlsSettings 663 | case "reality": 664 | realitySettings := map[string]interface{}{ 665 | "show": false, 666 | "fingerprint": vmess.GetValue(field.FingerPrint), 667 | "serverName": vmess.GetHostValue(field.SNI), 668 | "publicKey": vmess.GetValue(field.PublicKey), 669 | "shortId": vmess.GetValue(field.ShortId), 670 | "spiderX": vmess.GetValue(field.SpiderX), 671 | } 672 | streamSettings["realitySettings"] = realitySettings 673 | mux = false 674 | } 675 | switch network { 676 | case "tcp": 677 | streamSettings["tcpSettings"] = map[string]interface{}{ 678 | "header": map[string]interface{}{ 679 | "type": vmess.GetValue(field.TCPHeaderType), 680 | }, 681 | } 682 | case "kcp": 683 | kcpSettings := map[string]interface{}{ 684 | "mtu": 1350, 685 | "tti": 50, 686 | "uplinkCapacity": 12, 687 | "downlinkCapacity": 100, 688 | "congestion": false, 689 | "readBufferSize": 2, 690 | "writeBufferSize": 2, 691 | "header": map[string]interface{}{ 692 | "type": vmess.GetValue(field.MkcpHeaderType), 693 | }, 694 | } 695 | if vmess.Has(field.Seed.Key) { 696 | kcpSettings["seed"] = vmess.GetValue(field.Seed) 697 | } 698 | streamSettings["kcpSettings"] = kcpSettings 699 | case "h2": 700 | mux = false 701 | host := make([]string, 0) 702 | for _, line := range strings.Split(vmess.GetHostValue(field.H2Host), ",") { 703 | line = strings.TrimSpace(line) 704 | if line != "" { 705 | host = append(host, line) 706 | } 707 | } 708 | streamSettings["httpSettings"] = map[string]interface{}{ 709 | "path": vmess.GetValue(field.H2Path), 710 | "host": host, 711 | } 712 | case "ws": 713 | streamSettings["wsSettings"] = map[string]interface{}{ 714 | "path": vmess.GetValue(field.WsPath), 715 | "headers": map[string]interface{}{ 716 | "Host": vmess.GetValue(field.WsHost), 717 | }, 718 | } 719 | case "quic": 720 | quicSettings := map[string]interface{}{ 721 | "security": vmess.GetValue(field.QuicSecurity), 722 | "header": map[string]interface{}{ 723 | "type": vmess.GetValue(field.QuicHeaderType), 724 | }, 725 | } 726 | if vmess.GetValue(field.QuicSecurity) != "none" { 727 | quicSettings["key"] = vmess.GetValue(field.QuicKey) 728 | } 729 | streamSettings["quicSettings"] = quicSettings 730 | case "grpc": 731 | streamSettings["grpcSettings"] = map[string]interface{}{ 732 | "serviceName": vmess.GetValue(field.GrpcServiceName), 733 | "multiMode": vmess.GetValue(field.GrpcMode) == "multi", 734 | } 735 | } 736 | return map[string]interface{}{ 737 | "tag": "proxy", 738 | "protocol": "vmess", 739 | "settings": map[string]interface{}{ 740 | "vnext": []interface{}{ 741 | map[string]interface{}{ 742 | "address": vmess.Address, 743 | "port": vmess.Port, 744 | "users": []interface{}{ 745 | map[string]interface{}{ 746 | "id": vmess.ID, 747 | "security": vmess.GetValue(field.VMessEncryption), 748 | "level": 0, 749 | }, 750 | }, 751 | }, 752 | }, 753 | }, 754 | "streamSettings": streamSettings, 755 | "mux": map[string]interface{}{ 756 | "enabled": mux, 757 | }, 758 | } 759 | } 760 | -------------------------------------------------------------------------------- /cmd/node.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "Txray/cmd/help" 5 | "Txray/core" 6 | "Txray/core/manage" 7 | "Txray/core/node" 8 | "Txray/core/protocols" 9 | "Txray/core/sub" 10 | "Txray/log" 11 | "fmt" 12 | "io/ioutil" 13 | "net/url" 14 | "os" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/abiosoft/ishell" 19 | "github.com/atotto/clipboard" 20 | "github.com/olekukonko/tablewriter" 21 | ) 22 | 23 | func InitNodeShell(shell *ishell.Shell) { 24 | nodeCmd := &ishell.Cmd{ 25 | Name: "node", 26 | Func: func(c *ishell.Context) { 27 | argMap := FlagsParse(c.Args, map[string]string{ 28 | "d": "desc", 29 | }) 30 | key := "all" 31 | if _, ok := argMap["data"]; ok { 32 | key = argMap["data"] 33 | } 34 | _, isDesc := argMap["desc"] 35 | table := tablewriter.NewWriter(os.Stdout) 36 | table.SetHeader([]string{"索引", "协议", "别名", "地址", "端口", "测试结果"}) 37 | table.SetAlignment(tablewriter.ALIGN_CENTER) 38 | center := tablewriter.ALIGN_CENTER 39 | left := tablewriter.ALIGN_LEFT 40 | table.SetColumnAlignment([]int{center, center, left, center, center, center}) 41 | table.SetColWidth(70) 42 | indexList := core.IndexList(key, manage.Manager.NodeLen()) 43 | if isDesc { 44 | indexList = core.Reverse(indexList) 45 | } 46 | for _, index := range indexList { 47 | n := manage.Manager.GetNode(index) 48 | if n != nil { 49 | table.Append([]string{ 50 | strconv.Itoa(index), 51 | string(n.GetProtocolMode()), 52 | n.GetName(), 53 | n.GetAddr(), 54 | strconv.Itoa(n.GetPort()), 55 | n.TestResultStr(), 56 | }) 57 | } 58 | } 59 | if n := manage.Manager.SelectedNode(); n == nil { 60 | table.SetCaption(true, fmt.Sprintf("[ %d/%d ] %s", 61 | manage.Manager.SelectedIndex(), 62 | manage.Manager.NodeLen(), 63 | "无节点", 64 | )) 65 | } else { 66 | table.SetCaption(true, fmt.Sprintf("[ %d/%d ] %s", 67 | manage.Manager.SelectedIndex(), 68 | manage.Manager.NodeLen(), 69 | n.GetName(), 70 | )) 71 | } 72 | table.Render() 73 | }, 74 | } 75 | // help 76 | nodeCmd.AddCmd(&ishell.Cmd{ 77 | Name: "help", 78 | Aliases: []string{"-h", "--help"}, 79 | Help: "查看帮助", 80 | Func: func(c *ishell.Context) { 81 | c.Println(help.Node) 82 | }, 83 | }) 84 | // tcping 85 | nodeCmd.AddCmd(&ishell.Cmd{ 86 | Name: "tcping", 87 | Func: func(c *ishell.Context) { 88 | manage.Manager.Tcping() 89 | _ = shell.Process("node", "-d") 90 | }, 91 | }) 92 | // info 93 | nodeCmd.AddCmd(&ishell.Cmd{ 94 | Name: "info", 95 | Func: func(c *ishell.Context) { 96 | if len(c.Args) == 1 { 97 | v, err := strconv.Atoi(c.Args[0]) 98 | if err != nil { 99 | log.Warn("非法参数") 100 | return 101 | } 102 | n := manage.Manager.GetNode(v) 103 | if n == nil { 104 | log.Warn("不存在该节点") 105 | return 106 | } 107 | n.Show() 108 | } 109 | }, 110 | }) 111 | // rm 112 | nodeCmd.AddCmd(&ishell.Cmd{ 113 | Name: "rm", 114 | Func: func(c *ishell.Context) { 115 | if len(c.Args) == 1 { 116 | manage.Manager.DelNode(c.Args[0]) 117 | } 118 | }, 119 | }) 120 | // sort 121 | nodeCmd.AddCmd(&ishell.Cmd{ 122 | Name: "sort", 123 | Func: func(c *ishell.Context) { 124 | if len(c.Args) == 1 { 125 | var mode int 126 | switch c.Args[0] { 127 | case "0": 128 | mode = 0 129 | case "1": 130 | mode = 1 131 | case "2": 132 | mode = 2 133 | case "3": 134 | mode = 3 135 | case "4": 136 | mode = 4 137 | case "5": 138 | mode = 5 139 | default: 140 | return 141 | } 142 | manage.Manager.Sort(mode) 143 | _ = shell.Process("node") 144 | } 145 | }, 146 | }) 147 | // export 148 | nodeCmd.AddCmd(&ishell.Cmd{ 149 | Name: "export", 150 | Func: func(c *ishell.Context) { 151 | argMap := FlagsParse(c.Args, map[string]string{ 152 | "c": "clipboard", 153 | }) 154 | key := "all" 155 | if _, ok := argMap["data"]; ok { 156 | key = argMap["data"] 157 | } 158 | links := manage.Manager.GetNodeLink(key) 159 | if _, ok := argMap["clipboard"]; ok { 160 | err := clipboard.WriteAll(strings.Join(links, "\n")) 161 | if err != nil { 162 | log.Error(err) 163 | return 164 | } 165 | c.Println("共导出", len(links), "条至剪贴板") 166 | } else { 167 | c.Println("========================================================================") 168 | c.Println(strings.Join(links, "\n")) 169 | c.Println("========================================================================") 170 | c.Println("共导出", len(links), "条") 171 | } 172 | }, 173 | }) 174 | // find 175 | nodeCmd.AddCmd(&ishell.Cmd{ 176 | Name: "find", 177 | Func: func(c *ishell.Context) { 178 | if len(c.Args) != 1 { 179 | return 180 | } 181 | table := tablewriter.NewWriter(os.Stdout) 182 | table.SetHeader([]string{"索引", "协议", "别名", "地址", "端口", "测试结果"}) 183 | table.SetAlignment(tablewriter.ALIGN_CENTER) 184 | center := tablewriter.ALIGN_CENTER 185 | left := tablewriter.ALIGN_LEFT 186 | table.SetColumnAlignment([]int{center, center, left, center, center, center}) 187 | table.SetColWidth(70) 188 | manage.Manager.NodeForEach(func(i int, n *node.Node) { 189 | if n != nil && strings.Contains(n.GetName(), c.Args[0]) { 190 | defer table.Append([]string{ 191 | strconv.Itoa(i), 192 | string(n.GetProtocolMode()), 193 | n.GetName(), 194 | n.GetAddr(), 195 | strconv.Itoa(n.GetPort()), 196 | n.TestResultStr(), 197 | }) 198 | } 199 | }) 200 | if n := manage.Manager.SelectedNode(); n == nil { 201 | table.SetCaption(true, fmt.Sprintf("[ %d/%d ] %s", 202 | manage.Manager.SelectedIndex(), 203 | manage.Manager.NodeLen(), 204 | "无节点", 205 | )) 206 | } else { 207 | table.SetCaption(true, fmt.Sprintf("[ %d/%d ] %s", 208 | manage.Manager.SelectedIndex(), 209 | manage.Manager.NodeLen(), 210 | n.GetName(), 211 | )) 212 | } 213 | table.Render() 214 | }, 215 | }) 216 | // add 217 | nodeCmd.AddCmd(&ishell.Cmd{ 218 | Name: "add", 219 | Func: func(c *ishell.Context) { 220 | argMap := FlagsParse(c.Args, map[string]string{ 221 | "l": "link", 222 | "c": "clipboard", 223 | "f": "file", 224 | }) 225 | // 从剪贴板导入节点 226 | if _, ok := argMap["clipboard"]; ok { 227 | content, err := clipboard.ReadAll() 228 | if err != nil { 229 | log.Error(err) 230 | return 231 | } 232 | content = strings.ReplaceAll(content, "\r\n", "\n") 233 | content = strings.ReplaceAll(content, "\r", "\n") 234 | c.Println("剪贴板内容如下:") 235 | c.Println("========================================================================") 236 | c.Println(content) 237 | c.Println("========================================================================") 238 | if strings.Contains(content, "://") { 239 | for _, link := range strings.Split(content, "\n") { 240 | manage.Manager.AddNode(node.NewNode(link, "")) 241 | } 242 | } else { 243 | for _, link := range sub.Sub2links(content) { 244 | manage.Manager.AddNode(node.NewNode(link, "")) 245 | } 246 | } 247 | } else if fileArg, ok := argMap["file"]; ok { 248 | if _, err := os.Stat(fileArg); os.IsNotExist(err) { 249 | log.Error("open ", fileArg, " : 没有这个文件") 250 | return 251 | } 252 | data, _ := ioutil.ReadFile(fileArg) 253 | content := strings.ReplaceAll(string(data), "\r\n", "\n") 254 | content = strings.ReplaceAll(content, "\r", "\n") 255 | c.Println("文件内容如下:") 256 | c.Println("========================================================================") 257 | c.Println(content) 258 | c.Println("========================================================================") 259 | if strings.Contains(content, "://") { 260 | for _, link := range strings.Split(content, "\n") { 261 | manage.Manager.AddNode(node.NewNode(link, "")) 262 | } 263 | } else { 264 | for _, link := range sub.Sub2links(content) { 265 | manage.Manager.AddNode(node.NewNode(link, "")) 266 | } 267 | } 268 | } else if linkArg, ok := argMap["link"]; ok { 269 | manage.Manager.AddNode(node.NewNode(linkArg, "")) 270 | } else { 271 | c.ShowPrompt(false) 272 | defer c.ShowPrompt(true) 273 | modeList := []string{ 274 | protocols.ModeVMess.String(), 275 | protocols.ModeVLESS.String(), 276 | protocols.ModeVMessAEAD.String(), 277 | protocols.ModeTrojan.String(), 278 | protocols.ModeShadowSocks.String(), 279 | protocols.ModeSocks.String(), 280 | "退出", 281 | } 282 | i := c.MultiChoice(modeList, "手动添加何种协议的节点?") 283 | protocolMode := modeList[i] 284 | switch protocolMode { 285 | case protocols.ModeVMessAEAD.String(): 286 | c.Println("========================") 287 | c.Println(protocolMode) 288 | c.Println("========================") 289 | c.Print("别名(remarks): ") 290 | remarks := c.ReadLine() 291 | c.Print("地址(address): ") 292 | address := c.ReadLine() 293 | c.Print("端口(port): ") 294 | port, err := strconv.Atoi(c.ReadLine()) 295 | if err != nil || port < 1 || port > 65535 { 296 | log.Warn("端口为数字,且取值为1~65535") 297 | return 298 | } 299 | c.Print("用户ID(id): ") 300 | id := c.ReadLine() 301 | data := make(map[string]string) 302 | networkList := []string{ 303 | "tcp", 304 | "kcp", 305 | "ws", 306 | "h2", 307 | "quic", 308 | "grpc", 309 | } 310 | index := c.MultiChoice(networkList, "传输协议(network)?") 311 | network := networkList[index] 312 | if network != "tcp" { 313 | data["type"] = network 314 | } 315 | switch network { 316 | case "kcp": 317 | typeList := []string{ 318 | "none", 319 | "srtp", 320 | "utp", 321 | "wechat-video", 322 | "dtls", 323 | "wireguard", 324 | } 325 | index := c.MultiChoice(typeList, "伪装类型(headerType) ?") 326 | if typeList[index] != "none" { 327 | data["headerType"] = typeList[index] 328 | } 329 | c.Print("KCP种子(seed): ") 330 | seed := c.ReadLine() 331 | if seed != "" { 332 | data["seed"] = seed 333 | } 334 | case "ws": 335 | c.Print("WebSocket 的路径(path): ") 336 | path := c.ReadLine() 337 | if path != "" { 338 | data["path"] = path 339 | } 340 | c.Print("WebSocket Host(host): ") 341 | host := c.ReadLine() 342 | if host != "" { 343 | data["host"] = host 344 | } 345 | case "h2": 346 | c.Print("HTTP/2 的路径(path): ") 347 | path := c.ReadLine() 348 | if path != "" { 349 | data["path"] = path 350 | } 351 | c.Print("HTTP/2 Host(host): ") 352 | host := c.ReadLine() 353 | if host != "" { 354 | data["host"] = host 355 | } 356 | case "quic": 357 | typeList := []string{ 358 | "none", 359 | "srtp", 360 | "utp", 361 | "wechat-video", 362 | "dtls", 363 | "wireguard", 364 | } 365 | index := c.MultiChoice(typeList, "伪装类型(headerType)?") 366 | data["headerType"] = typeList[index] 367 | quicSecurityList := []string{ 368 | "none", 369 | "aes-128-gcm", 370 | "chacha20-poly1305", 371 | } 372 | index = c.MultiChoice(quicSecurityList, "QUIC 的加密方式(quicSecurity)?") 373 | quicSecurity := quicSecurityList[index] 374 | data["quicSecurity"] = quicSecurity 375 | if quicSecurity != "none" { 376 | c.Print("当 QUIC 的加密方式不为 none 时的加密密钥(key): ") 377 | key := c.ReadLine() 378 | if key == "" { 379 | log.Warn("加密密钥不能为空") 380 | return 381 | } 382 | data["key"] = key 383 | } 384 | case "grpc": 385 | c.Print("gRPC 的 ServiceName(serviceName): ") 386 | serviceName := c.ReadLine() 387 | if serviceName != "" { 388 | data["serviceName"] = serviceName 389 | } 390 | grpcModeList := []string{ 391 | "gun", 392 | "multi", 393 | } 394 | index := c.MultiChoice(grpcModeList, "gRPC 的传输模式(mode)?") 395 | mode := grpcModeList[index] 396 | if mode != "gun" { 397 | data["mode"] = mode 398 | } 399 | } 400 | securityList := []string{ 401 | "", 402 | "tls", 403 | "reality", 404 | } 405 | security_index := c.MultiChoice(securityList, "底层传输安全(security)?") 406 | security := securityList[security_index] 407 | switch security { 408 | case "": 409 | case "tls": 410 | data["security"] = security 411 | c.Print("SNI(sni): ") 412 | sni := c.ReadLine() 413 | if sni != "" { 414 | data["sni"] = sni 415 | } 416 | alpnList := []string{ 417 | "", 418 | "h2", 419 | "http/1.1", 420 | "h2,http/1.1", 421 | } 422 | index := c.MultiChoice(alpnList, "Alpn ?") 423 | if alpnList[index] != "" { 424 | data["alpn"] = alpnList[index] 425 | } 426 | case "reality": 427 | data["security"] = security 428 | c.Print("SNI(sni): ") 429 | sni := c.ReadLine() 430 | if sni != "" { 431 | data["sni"] = sni 432 | } 433 | fpList := []string{ 434 | "", 435 | "chrome", 436 | "firefox", 437 | "safari", 438 | "ios", 439 | "android", 440 | "edge", 441 | "360", 442 | "qq", 443 | "random", 444 | "randomized", 445 | } 446 | index := c.MultiChoice(fpList, "指纹(FingerPrint) ?") 447 | if fpList[index] != "" { 448 | data["fp"] = fpList[index] 449 | } 450 | c.Print("公钥(PublicKey): ") 451 | data["pbk"] = c.ReadLine() 452 | c.Print("ShortId: ") 453 | data["sid"] = c.ReadLine() 454 | c.Print("SpiderX: ") 455 | data["spx"] = c.ReadLine() 456 | } 457 | vmessAEAD := &protocols.VMessAEAD{ 458 | ID: id, 459 | Address: address, 460 | Port: port, 461 | Remarks: remarks, 462 | Values: url.Values{}, 463 | } 464 | for k, v := range data { 465 | vmessAEAD.Values[k] = []string{v} 466 | } 467 | if manage.Manager.AddNode(node.NewNodeByData(vmessAEAD)) { 468 | c.Println("添加成功") 469 | } 470 | case protocols.ModeVLESS.String(): 471 | c.Println("========================") 472 | c.Println(protocolMode) 473 | c.Println("========================") 474 | c.Print("别名(remarks): ") 475 | remarks := c.ReadLine() 476 | c.Print("地址(address): ") 477 | address := c.ReadLine() 478 | c.Print("端口(port): ") 479 | port, err := strconv.Atoi(c.ReadLine()) 480 | if err != nil || port < 1 || port > 65535 { 481 | log.Warn("端口为数字,且取值为1~65535") 482 | return 483 | } 484 | c.Print("用户ID(id): ") 485 | id := c.ReadLine() 486 | data := make(map[string]string) 487 | flowList := []string{ 488 | "", 489 | "xtls-rprx-vision", 490 | "xtls-rprx-vision-udp443", 491 | "xtls-rprx-origin", 492 | "xtls-rprx-origin-udp443", 493 | "xtls-rprx-direct", 494 | "xtls-rprx-direct-udp443", 495 | "xtls-rprx-splice", 496 | "xtls-rprx-splice-udp443", 497 | } 498 | index := c.MultiChoice(flowList, "流控(flow)?") 499 | flow := flowList[index] 500 | data["flow"] = flow 501 | networkList := []string{ 502 | "tcp", 503 | "kcp", 504 | "ws", 505 | "h2", 506 | "quic", 507 | "grpc", 508 | } 509 | index = c.MultiChoice(networkList, "传输协议(network)?") 510 | network := networkList[index] 511 | if network != "tcp" { 512 | data["type"] = network 513 | } 514 | switch network { 515 | case "tcp": 516 | typeList := []string{ 517 | "none", 518 | "http", 519 | } 520 | index = c.MultiChoice(typeList, "伪装类型(headerType) ?") 521 | if typeList[index] != "none" { 522 | data["headerType"] = typeList[index] 523 | } 524 | case "kcp": 525 | typeList := []string{ 526 | "none", 527 | "srtp", 528 | "utp", 529 | "wechat-video", 530 | "dtls", 531 | "wireguard", 532 | } 533 | index = c.MultiChoice(typeList, "伪装类型(headerType) ?") 534 | if typeList[index] != "none" { 535 | data["headerType"] = typeList[index] 536 | } 537 | c.Print("KCP种子(seed): ") 538 | seed := c.ReadLine() 539 | if seed != "" { 540 | data["seed"] = seed 541 | } 542 | case "ws": 543 | c.Print("WebSocket 的路径(path): ") 544 | path := c.ReadLine() 545 | if path != "" { 546 | data["path"] = path 547 | } 548 | c.Print("WebSocket Host(host): ") 549 | host := c.ReadLine() 550 | if host != "" { 551 | data["host"] = host 552 | } 553 | case "h2": 554 | c.Print("HTTP/2 的路径(path): ") 555 | path := c.ReadLine() 556 | if path != "" { 557 | data["path"] = path 558 | } 559 | c.Print("HTTP/2 Host(host): ") 560 | host := c.ReadLine() 561 | if host != "" { 562 | data["host"] = host 563 | } 564 | case "quic": 565 | typeList := []string{ 566 | "none", 567 | "srtp", 568 | "utp", 569 | "wechat-video", 570 | "dtls", 571 | "wireguard", 572 | } 573 | index = c.MultiChoice(typeList, "伪装类型(headerType)?") 574 | data["headerType"] = typeList[index] 575 | quicSecurityList := []string{ 576 | "none", 577 | "aes-128-gcm", 578 | "chacha20-poly1305", 579 | } 580 | index = c.MultiChoice(quicSecurityList, "QUIC 的加密方式(quicSecurity)?") 581 | quicSecurity := quicSecurityList[index] 582 | data["quicSecurity"] = quicSecurity 583 | if quicSecurity != "none" { 584 | c.Print("当 QUIC 的加密方式不为 none 时的加密密钥(key): ") 585 | key := c.ReadLine() 586 | if key == "" { 587 | log.Warn("加密密钥不能为空") 588 | return 589 | } 590 | data["key"] = key 591 | } 592 | case "grpc": 593 | c.Print("gRPC 的 ServiceName(serviceName): ") 594 | serviceName := c.ReadLine() 595 | if serviceName != "" { 596 | data["serviceName"] = serviceName 597 | } 598 | grpcModeList := []string{ 599 | "gun", 600 | "multi", 601 | } 602 | index = c.MultiChoice(grpcModeList, "gRPC 的传输模式(mode)?") 603 | mode := grpcModeList[index] 604 | if mode != "gun" { 605 | data["mode"] = mode 606 | } 607 | } 608 | 609 | securityList := []string{ 610 | "", 611 | "tls", 612 | "xtls", 613 | "reality", 614 | } 615 | index = c.MultiChoice(securityList, "底层传输安全(security)?") 616 | security := securityList[index] 617 | switch security { 618 | case "": 619 | case "tls": 620 | data["security"] = security 621 | c.Print("SNI(sni): ") 622 | sni := c.ReadLine() 623 | if sni != "" { 624 | data["sni"] = sni 625 | } 626 | alpnList := []string{ 627 | "", 628 | "h2", 629 | "http/1.1", 630 | "h2,http/1.1", 631 | } 632 | index := c.MultiChoice(alpnList, "Alpn ?") 633 | if alpnList[index] != "" { 634 | data["alpn"] = alpnList[index] 635 | } 636 | case "xtls": 637 | data["security"] = security 638 | c.Print("SNI(sni): ") 639 | sni := c.ReadLine() 640 | if sni != "" { 641 | data["sni"] = sni 642 | } 643 | alpnList := []string{ 644 | "", 645 | "h2", 646 | "http/1.1", 647 | "h2,http/1.1", 648 | } 649 | index := c.MultiChoice(alpnList, "Alpn ?") 650 | if alpnList[index] != "" { 651 | data["alpn"] = alpnList[index] 652 | } 653 | case "reality": 654 | data["security"] = security 655 | c.Print("SNI(sni): ") 656 | sni := c.ReadLine() 657 | if sni != "" { 658 | data["sni"] = sni 659 | } 660 | fpList := []string{ 661 | "", 662 | "chrome", 663 | "firefox", 664 | "safari", 665 | "ios", 666 | "android", 667 | "edge", 668 | "360", 669 | "qq", 670 | "random", 671 | "randomized", 672 | } 673 | index := c.MultiChoice(fpList, "指纹(FingerPrint) ?") 674 | if fpList[index] != "" { 675 | data["fp"] = fpList[index] 676 | } 677 | c.Print("公钥(PublicKey): ") 678 | data["pbk"] = c.ReadLine() 679 | c.Print("ShortId: ") 680 | data["sid"] = c.ReadLine() 681 | c.Print("SpiderX: ") 682 | data["spx"] = c.ReadLine() 683 | } 684 | vless := &protocols.VLess{ 685 | ID: id, 686 | Address: address, 687 | Port: port, 688 | Remarks: remarks, 689 | Values: url.Values{}, 690 | } 691 | for k, v := range data { 692 | vless.Values[k] = []string{v} 693 | } 694 | if manage.Manager.AddNode(node.NewNodeByData(vless)) { 695 | c.Println("添加成功") 696 | } 697 | case protocols.ModeVMess.String(): 698 | vmess := &protocols.VMess{V: "2"} 699 | c.Println("========================") 700 | c.Println(protocolMode) 701 | c.Println("========================") 702 | c.Print("别名(remarks): ") 703 | vmess.Ps = c.ReadLine() 704 | c.Print("地址(address): ") 705 | vmess.Add = c.ReadLine() 706 | c.Print("端口(port): ") 707 | port, err := strconv.Atoi(c.ReadLine()) 708 | if err != nil || port < 1 || port > 65535 { 709 | log.Warn("端口为数字,且取值为1~65535") 710 | return 711 | } 712 | vmess.Port = port 713 | c.Print("用户ID(id): ") 714 | vmess.Id = c.ReadLine() 715 | c.Print("额外ID(alterID): ") 716 | alterID, err := strconv.Atoi(c.ReadLine()) 717 | if err != nil { 718 | log.Warn("额外ID为数字") 719 | return 720 | } 721 | vmess.Aid = alterID 722 | securityList := []string{ 723 | "auto", 724 | "aes-128-gcm", 725 | "chacha20-poly1305", 726 | "none", 727 | "zero", 728 | } 729 | index := c.MultiChoice(securityList, "加密方式(security)?") 730 | vmess.Scy = securityList[index] 731 | networkList := []string{ 732 | "tcp", 733 | "kcp", 734 | "ws", 735 | "h2", 736 | "quic", 737 | "grpc", 738 | } 739 | index = c.MultiChoice(networkList, "传输协议(network)?") 740 | vmess.Net = networkList[index] 741 | switch networkList[index] { 742 | case "tcp": 743 | vmess.Type = "none" 744 | case "kcp": 745 | typeList := []string{ 746 | "none", 747 | "srtp", 748 | "utp", 749 | "wechat-video", 750 | "dtls", 751 | "wireguard", 752 | } 753 | index = c.MultiChoice(typeList, "伪装头部类型(type)?") 754 | vmess.Type = typeList[index] 755 | c.Print("mKCP 种子(path): ") 756 | vmess.Path = c.ReadLine() 757 | case "quic": 758 | typeList := []string{ 759 | "none", 760 | "srtp", 761 | "utp", 762 | "wechat-video", 763 | "dtls", 764 | "wireguard", 765 | } 766 | index = c.MultiChoice(typeList, "伪装类型(type)?") 767 | vmess.Type = typeList[index] 768 | quicSecurityList := []string{ 769 | "none", 770 | "aes-128-gcm", 771 | "chacha20-poly1305", 772 | } 773 | index = c.MultiChoice(quicSecurityList, "QUIC的加密方式(host)?") 774 | vmess.Host = quicSecurityList[index] 775 | if vmess.Host != "none" { 776 | c.Print("QUIC的加密key(path): ") 777 | vmess.Path = c.ReadLine() 778 | } 779 | 780 | case "ws", "h2": 781 | c.Print("Host(host): ") 782 | vmess.Host = c.ReadLine() 783 | c.Print("Path(path): ") 784 | vmess.Path = c.ReadLine() 785 | case "grpc": 786 | typeList := []string{ 787 | "gun", 788 | "multi", 789 | } 790 | index = c.MultiChoice(typeList, "gRPC的传输模式(type)?") 791 | vmess.Type = typeList[index] 792 | c.Print("gRPC的ServiceName(path): ") 793 | vmess.Path = c.ReadLine() 794 | } 795 | tlsList := []string{ 796 | "", 797 | "tls", 798 | } 799 | index = c.MultiChoice(tlsList, "底层安全传输 (tls)?") 800 | vmess.Tls = tlsList[index] 801 | if vmess.Tls != "" { 802 | c.Print("SNI: ") 803 | vmess.Sni = c.ReadLine() 804 | alpnList := []string{ 805 | "", 806 | "h2", 807 | "http/1.1", 808 | "h2,http/1.1", 809 | } 810 | index := c.MultiChoice(alpnList, "Alpn ?") 811 | vmess.Alpn = alpnList[index] 812 | } 813 | c.Println("========================") 814 | if manage.Manager.AddNode(node.NewNodeByData(vmess)) { 815 | c.Println("添加成功") 816 | } 817 | case protocols.ModeShadowSocks.String(): 818 | c.Println("========================") 819 | c.Println(protocolMode) 820 | c.Println("========================") 821 | c.Print("别名(remarks): ") 822 | remarks := c.ReadLine() 823 | c.Print("地址(address): ") 824 | addr := c.ReadLine() 825 | c.Print("端口(port): ") 826 | port, err := strconv.Atoi(c.ReadLine()) 827 | if err != nil || port < 1 || port > 65535 { 828 | log.Warn("端口为数字,且取值为1~65535") 829 | return 830 | } 831 | c.Print("密码(password): ") 832 | password := c.ReadLine() 833 | security := []string{ 834 | "aes-256-cfb", 835 | "aes-128-cfb", 836 | "chacha20", 837 | "chacha20-ietf", 838 | "aes-256-gcm", 839 | "aes-256-gcm", 840 | "chacha20-poly1305", 841 | "chacha20-ietf-poly1305", 842 | } 843 | sIndex := c.MultiChoice(security, "加密方式(security) ?") 844 | c.Println("========================") 845 | ss := &protocols.ShadowSocks{ 846 | Remarks: remarks, 847 | Password: password, 848 | Address: addr, 849 | Port: port, 850 | Method: security[sIndex], 851 | } 852 | if manage.Manager.AddNode(node.NewNodeByData(ss)) { 853 | c.Println("添加成功") 854 | } 855 | case protocols.ModeTrojan.String(): 856 | c.Println("========================") 857 | c.Println(protocolMode) 858 | c.Println("========================") 859 | c.Print("别名(remarks): ") 860 | remarks := c.ReadLine() 861 | c.Print("地址(address): ") 862 | addr := c.ReadLine() 863 | c.Print("端口(port): ") 864 | port, err := strconv.Atoi(c.ReadLine()) 865 | if err != nil || port < 1 || port > 65535 { 866 | log.Warn("端口为数字,且取值为1~65535") 867 | return 868 | } 869 | c.Print("密码(password): ") 870 | password := c.ReadLine() 871 | c.Print("SNI(sni)(可选): ") 872 | sni := c.ReadLine() 873 | c.Println("========================") 874 | trojan := &protocols.Trojan{ 875 | Remarks: remarks, 876 | Password: password, 877 | Address: addr, 878 | Port: port, 879 | } 880 | if sni != "" { 881 | trojan.Values = url.Values{ 882 | "sni": []string{sni}, 883 | } 884 | } 885 | if manage.Manager.AddNode(node.NewNodeByData(trojan)) { 886 | c.Println("添加成功") 887 | } 888 | case protocols.ModeSocks.String(): 889 | c.Println("========================") 890 | c.Println(protocolMode) 891 | c.Println("========================") 892 | c.Print("别名(remarks): ") 893 | remarks := c.ReadLine() 894 | c.Print("地址(address): ") 895 | addr := c.ReadLine() 896 | c.Print("端口(port): ") 897 | port, err := strconv.Atoi(c.ReadLine()) 898 | if err != nil || port < 1 || port > 65535 { 899 | log.Warn("端口为数字,且取值为1~65535") 900 | return 901 | } 902 | c.Print("用户名(username)(可选): ") 903 | username := c.ReadLine() 904 | c.Print("密码(password)(可选): ") 905 | password := c.ReadLine() 906 | c.Println("========================") 907 | socks := &protocols.Socks{ 908 | Remarks: remarks, 909 | Address: addr, 910 | Port: port, 911 | } 912 | if username != "" && password != "" { 913 | socks.Username = username 914 | socks.Password = password 915 | } 916 | if manage.Manager.AddNode(node.NewNodeByData(socks)) { 917 | c.Println("添加成功") 918 | } 919 | } 920 | } 921 | }, 922 | }) 923 | shell.AddCmd(nodeCmd) 924 | } 925 | --------------------------------------------------------------------------------