mirror of
https://github.com/e1732a364fed/v2ray_simple.git
synced 2025-10-29 11:12:28 +08:00
修订,重构代码, 修复dns的bug; 添加Dns的DoT功能.
修复dns配置中"特殊服务器" 无法被正确配置、使用的bug 将 proxy.Standard结构 移动到 项目根目录的 StandardConf. 将 proxy.AppConf, LoadTomlConfStr, LoadTomlConfFile 函数 移动到根目录 因为 StandardConf和 AppConf里包含很多App级别的配置, 不宜放到proxy子包中 将 proxy.RuleConf 移动到 netLayer 将 proxy.LoadRulesForRoutePolicy 移动到 netLayer 将 proxy.LoadDnsMachine 移动到 netLayer 在dnsquery失败后,会判断错误, 若发现是Read错误,则会试图重新拨号
This commit is contained in:
30
cli.go
30
cli.go
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/hahahrfool/v2ray_simple/netLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/proxy"
|
||||
"github.com/hahahrfool/v2ray_simple/proxy/vless"
|
||||
"github.com/hahahrfool/v2ray_simple/quic"
|
||||
@@ -60,7 +61,7 @@ func init() {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("你选择了 %q\n", result)
|
||||
fmt.Printf("你选择了 %s\n", result)
|
||||
|
||||
switch i {
|
||||
case 0:
|
||||
@@ -136,7 +137,7 @@ func runCli() {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("你选择了 %q\n", result)
|
||||
fmt.Printf("你选择了 %s\n", result)
|
||||
|
||||
if f := cliCmdList[i].F; f != nil {
|
||||
f()
|
||||
@@ -156,8 +157,8 @@ func generateConfigFileInteractively() {
|
||||
"将此次生成的配置投入运行(热加载)",
|
||||
}
|
||||
|
||||
confClient := proxy.Standard{}
|
||||
confServer := proxy.Standard{}
|
||||
confClient := StandardConf{}
|
||||
confServer := StandardConf{}
|
||||
|
||||
var clientStr, serverStr string
|
||||
|
||||
@@ -174,16 +175,16 @@ func generateConfigFileInteractively() {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("你选择了 %q\n", result)
|
||||
fmt.Printf("你选择了 %s\n", result)
|
||||
|
||||
generateConfStr := func() {
|
||||
|
||||
confClient.Route = []*proxy.RuleConf{{
|
||||
confClient.Route = []*netLayer.RuleConf{{
|
||||
DialTag: "direct",
|
||||
Domains: []string{"geosite:cn"},
|
||||
}}
|
||||
|
||||
confClient.App = &proxy.AppConf{MyCountryISO_3166: "CN"}
|
||||
confClient.App = &AppConf{MyCountryISO_3166: "CN"}
|
||||
|
||||
clientStr, err = utils.GetPurgedTomlStr(confClient)
|
||||
if err != nil {
|
||||
@@ -210,8 +211,8 @@ func generateConfigFileInteractively() {
|
||||
fmt.Printf("\n")
|
||||
|
||||
case 2: //clear
|
||||
confClient = proxy.Standard{}
|
||||
confServer = proxy.Standard{}
|
||||
confClient = StandardConf{}
|
||||
confServer = StandardConf{}
|
||||
clientStr = ""
|
||||
serverStr = ""
|
||||
case 3: //output
|
||||
@@ -310,7 +311,7 @@ func generateConfigFileInteractively() {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("你选择了 %q\n", result)
|
||||
fmt.Printf("你选择了 %s\n", result)
|
||||
|
||||
if i2 < 2 {
|
||||
confClient.Listen = append(confClient.Listen, &proxy.ListenConf{})
|
||||
@@ -374,7 +375,7 @@ func generateConfigFileInteractively() {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("你选择了 %q\n", result)
|
||||
fmt.Printf("你选择了 %s\n", result)
|
||||
|
||||
confClient.Dial = append(confClient.Dial, &proxy.DialConf{})
|
||||
clientDial := confClient.Dial[0]
|
||||
@@ -543,6 +544,9 @@ func generateConfigFileInteractively() {
|
||||
serverListen.TLSKey = "cert.key"
|
||||
serverListen.Insecure = true
|
||||
clientDial.Insecure = true
|
||||
|
||||
fmt.Printf("你选择了默认自签名证书, 这是不安全的, 我们不推荐. 所以自动生成证书这一步需要你一会再到交互模式里选择相应选项进行生成。 \n")
|
||||
|
||||
} else {
|
||||
fmt.Printf("请输入 cert路径\n")
|
||||
|
||||
@@ -609,7 +613,7 @@ func interactively_hotRemoveServerOrClient() {
|
||||
|
||||
var will_delete_index int
|
||||
|
||||
fmt.Printf("你选择了 %q\n", result)
|
||||
fmt.Printf("你选择了 %s\n", result)
|
||||
switch i {
|
||||
case 0:
|
||||
will_delete_listen = true
|
||||
@@ -707,7 +711,7 @@ func interactively_hotLoadConfigFile() {
|
||||
|
||||
fmt.Printf("你输入了 %s\n", fpath)
|
||||
|
||||
standardConf, err = proxy.LoadTomlConfFile(fpath)
|
||||
standardConf, err = LoadTomlConfFile(fpath)
|
||||
if err != nil {
|
||||
|
||||
log.Printf("can not load standard config file: %s\n", err)
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
var (
|
||||
cmdPrintSupportedProtocols bool
|
||||
//cmdGenerateUUID bool
|
||||
|
||||
interactive_mode bool
|
||||
nodownload bool
|
||||
@@ -28,7 +27,6 @@ var (
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&cmdPrintSupportedProtocols, "sp", false, "print supported protocols")
|
||||
//flag.BoolVar(&cmdGenerateUUID, "gu", false, "generate a random valid uuid string")
|
||||
flag.BoolVar(&interactive_mode, "i", false, "enable interactive commandline mode")
|
||||
flag.BoolVar(&nodownload, "nd", false, "don't automatically download any extra data files")
|
||||
flag.BoolVar(&cmdPrintVer, "v", false, "print the version string then exit")
|
||||
@@ -85,11 +83,6 @@ func runPreCommands() {
|
||||
|
||||
}
|
||||
|
||||
//if cmdGenerateUUID {
|
||||
// generateAndPrintUUID()
|
||||
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
func generateAndPrintUUID() {
|
||||
@@ -179,7 +172,7 @@ func tryDownloadGeositeSourceFromConfiguredProxy() {
|
||||
protocol = "http"
|
||||
`
|
||||
|
||||
clientConf, err := proxy.LoadTomlConfStr(tempClientConfStr)
|
||||
clientConf, err := LoadTomlConfStr(tempClientConfStr)
|
||||
if err != nil {
|
||||
fmt.Println("can not create LoadTomlConfStr: ", err)
|
||||
|
||||
|
||||
51
configs.go
51
configs.go
@@ -3,10 +3,13 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/hahahrfool/v2ray_simple/httpLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/netLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/proxy"
|
||||
@@ -21,6 +24,48 @@ func init() {
|
||||
flag.IntVar(&jsonMode, "jm", 0, "json mode, 0:verysimple mode; 1: v2ray mode(not implemented yet)")
|
||||
}
|
||||
|
||||
type AppConf struct {
|
||||
LogLevel *int `toml:"loglevel"` //需要为指针, 否则无法判断0到底是未给出的默认值还是 显式声明的0
|
||||
DefaultUUID string `toml:"default_uuid"`
|
||||
MyCountryISO_3166 string `toml:"mycountry" json:"mycountry"` //加了mycountry后,就会自动按照geoip分流,也会对顶级域名进行国别分流
|
||||
|
||||
NoReadV bool `toml:"noreadv"`
|
||||
|
||||
AdminPass string `toml:"admin_pass"`
|
||||
}
|
||||
|
||||
//标准配置。默认使用toml格式
|
||||
// toml:https://toml.io/cn/
|
||||
// english: https://toml.io/en/
|
||||
type StandardConf struct {
|
||||
App *AppConf `toml:"app"`
|
||||
DnsConf *netLayer.DnsConf `toml:"dns"`
|
||||
|
||||
Listen []*proxy.ListenConf `toml:"listen"`
|
||||
Dial []*proxy.DialConf `toml:"dial"`
|
||||
|
||||
Route []*netLayer.RuleConf `toml:"route"`
|
||||
Fallbacks []*httpLayer.FallbackConf `toml:"fallback"`
|
||||
}
|
||||
|
||||
func LoadTomlConfStr(str string) (c StandardConf, err error) {
|
||||
_, err = toml.Decode(str, &c)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func LoadTomlConfFile(fileNamePath string) (StandardConf, error) {
|
||||
|
||||
if cf, err := os.Open(fileNamePath); err == nil {
|
||||
defer cf.Close()
|
||||
bs, _ := ioutil.ReadAll(cf)
|
||||
return LoadTomlConfStr(string(bs))
|
||||
} else {
|
||||
return StandardConf{}, utils.ErrInErr{ErrDesc: "can't open config file", ErrDetail: err}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//mainfallback, dnsMachine, routePolicy
|
||||
func loadCommonComponentsFromStandardConf() {
|
||||
|
||||
@@ -29,7 +74,7 @@ func loadCommonComponentsFromStandardConf() {
|
||||
}
|
||||
|
||||
if dnsConf := standardConf.DnsConf; dnsConf != nil {
|
||||
dnsMachine = proxy.LoadDnsMachine(dnsConf)
|
||||
dnsMachine = netLayer.LoadDnsMachine(dnsConf)
|
||||
}
|
||||
|
||||
hasAppLevelMyCountry := (standardConf.App != nil && standardConf.App.MyCountryISO_3166 != "")
|
||||
@@ -44,7 +89,7 @@ func loadCommonComponentsFromStandardConf() {
|
||||
|
||||
}
|
||||
|
||||
proxy.LoadRulesForRoutePolicy(standardConf.Route, routePolicy)
|
||||
netLayer.LoadRulesForRoutePolicy(standardConf.Route, routePolicy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +102,7 @@ func loadConfig() (err error) {
|
||||
|
||||
ext := filepath.Ext(fpath)
|
||||
if ext == ".toml" {
|
||||
standardConf, err = proxy.LoadTomlConfFile(fpath)
|
||||
standardConf, err = LoadTomlConfFile(fpath)
|
||||
if err != nil {
|
||||
|
||||
log.Printf("can not load standard config file: %s\n", err)
|
||||
|
||||
@@ -22,7 +22,7 @@ servers = [
|
||||
"udp://114.114.114.114:53", # 如果把该url指向我们dokodemo监听的端口,就可以达到通过节点请求dns的目的.
|
||||
#"udp://127.0.0.1:63782", # 如这一行 就是通过我们节点请求dns
|
||||
|
||||
{ addr = "udp://1.1.1.1:53", domains = [ "google.com" ] } # 还可以为特定域名指定特定服务器
|
||||
{ addr = "udp://8.8.8.8:53", domain = [ "google.com" ] } # 还可以为特定域名指定特定服务器
|
||||
]
|
||||
|
||||
[dns.hosts] # 自己定义的dns解析
|
||||
|
||||
2
main.go
2
main.go
@@ -45,7 +45,7 @@ var (
|
||||
|
||||
confMode int = -1 //0: simple json, 1: standard toml, 2: v2ray compatible json
|
||||
simpleConf proxy.Simple
|
||||
standardConf proxy.Standard
|
||||
standardConf StandardConf
|
||||
directClient, _, _ = proxy.ClientFromURL("direct://")
|
||||
defaultOutClient proxy.Client
|
||||
default_uuid string
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package netLayer
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -230,6 +231,7 @@ func (a *Addr) GetNetIPAddr() (na netip.Addr) {
|
||||
return
|
||||
}
|
||||
|
||||
//a.Network == "udp", "udp4", "udp6"
|
||||
func (a *Addr) IsUDP() bool {
|
||||
return IsStrUDP_network(a.Network)
|
||||
}
|
||||
@@ -253,11 +255,19 @@ func (a *Addr) HostStr() string {
|
||||
|
||||
func (addr *Addr) Dial() (net.Conn, error) {
|
||||
//log.Println("Dial called", addr, addr.Network)
|
||||
var istls bool
|
||||
var resultConn net.Conn
|
||||
var err error
|
||||
|
||||
switch addr.Network {
|
||||
case "":
|
||||
addr.Network = "tcp"
|
||||
goto tcp
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
goto tcp
|
||||
case "tls": //此形式目前被用于dns配置中 的 dns over tls 的 url中
|
||||
istls = true
|
||||
goto tcp
|
||||
case "udp", "udp4", "udp6":
|
||||
ua := addr.ToUDPAddr()
|
||||
|
||||
@@ -267,36 +277,56 @@ func (addr *Addr) Dial() (net.Conn, error) {
|
||||
|
||||
return DialUDP(ua)
|
||||
default:
|
||||
if strings.HasPrefix(addr.Network, "tcp") {
|
||||
goto tcp
|
||||
}
|
||||
|
||||
goto defaultPart
|
||||
|
||||
}
|
||||
|
||||
tcp:
|
||||
|
||||
if addr.IP != nil {
|
||||
if addr.IP.To4() == nil {
|
||||
if !machineCanConnectToIpv6 {
|
||||
return nil, ErrMachineCantConnectToIpv6
|
||||
} else {
|
||||
|
||||
return net.DialTCP("tcp6", nil, &net.TCPAddr{
|
||||
resultConn, err = net.DialTCP("tcp6", nil, &net.TCPAddr{
|
||||
IP: addr.IP,
|
||||
Port: addr.Port,
|
||||
})
|
||||
goto dialedPart
|
||||
}
|
||||
} else {
|
||||
|
||||
return net.DialTCP("tcp4", nil, &net.TCPAddr{
|
||||
resultConn, err = net.DialTCP("tcp4", nil, &net.TCPAddr{
|
||||
IP: addr.IP,
|
||||
Port: addr.Port,
|
||||
})
|
||||
goto dialedPart
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
defaultPart:
|
||||
return net.Dial(addr.Network, addr.String())
|
||||
resultConn, err = net.Dial(addr.Network, addr.String())
|
||||
|
||||
dialedPart:
|
||||
if istls && err == nil {
|
||||
|
||||
conf := &tls.Config{}
|
||||
|
||||
if addr.Name != "" {
|
||||
conf.ServerName = addr.Name
|
||||
} else {
|
||||
conf.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
tlsconn := tls.Client(resultConn, conf)
|
||||
err = tlsconn.Handshake()
|
||||
return tlsconn, err
|
||||
}
|
||||
return resultConn, err
|
||||
|
||||
}
|
||||
|
||||
// 如果a的ip不为空,则会返回 AtypIP4 或 AtypIP6,否则会返回 AtypDomain
|
||||
|
||||
238
netLayer/dns.go
238
netLayer/dns.go
@@ -1,8 +1,10 @@
|
||||
package netLayer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -13,12 +15,46 @@ import (
|
||||
|
||||
var globalDnsQueryMutex sync.Mutex
|
||||
|
||||
var ErrRecursion = errors.New("multiple recursion not allowed")
|
||||
|
||||
// 判断 DNSQuery 返回的错误 是否是 Read底层连接 的错误
|
||||
func Is_DNSQuery_returnType_ReadErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch err {
|
||||
case os.ErrNotExist, dns.ErrRcode, ErrRecursion:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//筛除掉 Is_DNSQuery_returnType_ReadErr 时,err 为 net.Error.Timeout() 的情况
|
||||
func Is_DNSQuery_returnType_ReadFatalErr(err error) bool {
|
||||
if !Is_DNSQuery_returnType_ReadErr(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ne, ok := err.(net.Error); ok {
|
||||
if ne.Timeout() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//domain必须是 dns.Fqdn 函数 包过的, 本函数不检查是否包过。如果不包过就传入,会报错。
|
||||
// dns_type 为 miekg/dns 包中定义的类型, 如 TypeA, TypeAAAA, TypeCNAME.
|
||||
// conn是一个建立好的 dns.Conn, 必须非空, 本函数不检查.
|
||||
// theMux是与 conn相匹配的mutex, 这是为了防止同时有多个请求导致无法对口;内部若判断为nil,会主动使用一个全局mux.
|
||||
// recursionCount 使用者统一填0 即可,用于内部 遇到cname时进一步查询时防止无限递归.
|
||||
func DNSQuery(domain string, dns_type uint16, conn *dns.Conn, theMux *sync.Mutex, recursionCount int) net.IP {
|
||||
//
|
||||
// 如果从conn中Read后成功返回, 则可能返回如下几种错误 os.ErrNotExist (表示查无此记录), dns.ErrRcode (表示dns返回的 Rcode 不是 dns.RcodeSuccess), ErrRecursion,
|
||||
// 如果不是这三个error, 那就是 从 该 conn 读取数据时出错了.
|
||||
func DNSQuery(domain string, dns_type uint16, conn *dns.Conn, theMux *sync.Mutex, recursionCount int) (net.IP, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion((domain), dns_type) //为了更快,不使用 dns.Fqdn, 请调用之前先确保ok
|
||||
c := new(dns.Client)
|
||||
@@ -37,7 +73,7 @@ func DNSQuery(domain string, dns_type uint16, conn *dns.Conn, theMux *sync.Mutex
|
||||
if ce := utils.CanLogErr("dns query read err"); ce != nil {
|
||||
ce.Write(zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
@@ -45,20 +81,20 @@ func DNSQuery(domain string, dns_type uint16, conn *dns.Conn, theMux *sync.Mutex
|
||||
//dns查不到的情况是很有可能的,所以还是放在debug日志里
|
||||
ce.Write(zap.Error(err), zap.Int("rcode", r.Rcode), zap.String("value", r.String()))
|
||||
}
|
||||
return nil
|
||||
return nil, dns.ErrRcode
|
||||
}
|
||||
|
||||
switch dns_type {
|
||||
case dns.TypeA:
|
||||
for _, a := range r.Answer {
|
||||
if aa, ok := a.(*dns.A); ok {
|
||||
return aa.A
|
||||
return aa.A, nil
|
||||
}
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
for _, a := range r.Answer {
|
||||
if aa, ok := a.(*dns.AAAA); ok {
|
||||
return aa.AAAA
|
||||
return aa.AAAA, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,20 +112,24 @@ func DNSQuery(domain string, dns_type uint16, conn *dns.Conn, theMux *sync.Mutex
|
||||
if ce := utils.CanLogDebug("dns query got cname but recursionCount>2"); ce != nil {
|
||||
ce.Write(zap.String("query", domain), zap.String("cname", aa.Target))
|
||||
}
|
||||
return nil
|
||||
return nil, ErrRecursion
|
||||
}
|
||||
return DNSQuery(dns.Fqdn(aa.Target), dns_type, conn, theMux, recursionCount+1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// 给 miekg/dns.Conn 加一个互斥锁, 可保证同一时间仅有一个请求发生
|
||||
// 这样就不会造成并发时的混乱
|
||||
type DnsConn struct {
|
||||
*dns.Conn
|
||||
Name string //我们这里惯例,直接使用配置文件中配置的url字符串作为Name
|
||||
raddr *Addr //这个用于在Conn出故障后, 重新拨号时所使用
|
||||
mutex sync.Mutex
|
||||
|
||||
garbageMark bool
|
||||
}
|
||||
|
||||
//dns machine维持与多个dns服务器的连接(最好是udp这种无状态的),并可以发起dns请求。
|
||||
@@ -98,9 +138,9 @@ type DnsConn struct {
|
||||
// SpecialServerPollicy 用于为特殊的 域名指定特殊的 dns服务器,这样遇到这种域名时,会通过该特定服务器查询
|
||||
type DNSMachine struct {
|
||||
TypeStrategy int64 // 0, 4, 6, 40, 60
|
||||
DefaultConn DnsConn
|
||||
defaultConn DnsConn
|
||||
conns map[string]*DnsConn
|
||||
cache map[string]net.IP
|
||||
cache map[string]net.IP //cache的key统一为 未经 Fqdn包装过的域名. 即尾部没有点号
|
||||
|
||||
SpecialIPPollicy map[string][]netip.Addr
|
||||
|
||||
@@ -110,63 +150,89 @@ type DNSMachine struct {
|
||||
|
||||
}
|
||||
|
||||
//并不初始化所有内部成员, 只是创建空结构并拨号,若为nil则号也不拨
|
||||
func NewDnsMachine(defaultDnsServerAddr *Addr) *DNSMachine {
|
||||
var dm DNSMachine
|
||||
if defaultDnsServerAddr != nil {
|
||||
// Dial通过 c 内部设置好的地址进行拨号,并将 c.Conn.Conn 设为 新建立好的连接
|
||||
func (c *DnsConn) Dial() error {
|
||||
nc, err := DialDnsAddr(c.raddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Conn.Conn = nc
|
||||
return nil
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
//建立一个与dns服务器连接, 可为纯udp的dns, 或者 DoT的. 如果是DoT的, 则要求 addr.Network == "tls",
|
||||
// 如果是纯udp的,要求 addr.IsUDP() == true
|
||||
func DialDnsAddr(addr *Addr) (conn net.Conn, err error) {
|
||||
|
||||
//实测 miekg/dns 必须用 net.PacketConn, 不过本作udp最新代码已经支持了.
|
||||
// 不过dns还是没必要额外包装一次, 直接用原始的udp即可.
|
||||
|
||||
//在 miekg/dns 遇到非 net.PacketConn 的连接时,会采用不同的办法,先从数据读取一个长度信息,然后再读其它信息,可能它没有料到 net.Conn 被包装的情况
|
||||
|
||||
if defaultDnsServerAddr.IsUDP() {
|
||||
conn, err = net.DialUDP("udp", nil, defaultDnsServerAddr.ToUDPAddr())
|
||||
/*
|
||||
dns over tls rfc:https://datatracker.ietf.org/doc/html/rfc7858
|
||||
853端口
|
||||
|
||||
根据
|
||||
https://datatracker.ietf.org/doc/html/rfc7858#section-3.3
|
||||
|
||||
每个信息之前都要传2字节的信息长度
|
||||
|
||||
所以显然 miekg/dns 认为传入的conn不是 net.UDPConn 就是 tls.Conn
|
||||
|
||||
另外,miekg/dns 不支持 doh, 证据在 https://github.com/miekg/dns/pull/800
|
||||
|
||||
就是因为 doh完全和 dot不同,使用了不同的数据结构.
|
||||
*/
|
||||
|
||||
if addr.IsUDP() {
|
||||
conn, err = net.DialUDP("udp", nil, addr.ToUDPAddr())
|
||||
} else {
|
||||
conn, err = defaultDnsServerAddr.Dial()
|
||||
conn, err = addr.Dial()
|
||||
|
||||
}
|
||||
//todo: 以后支持DoH的话,要分离出https这个Network然后单独使用独特方法进行dial
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (dm *DNSMachine) SetDefaultConn(c net.Conn, addr *Addr) {
|
||||
dm.defaultConn.Conn = new(dns.Conn)
|
||||
dm.defaultConn.Conn.Conn = c
|
||||
dm.defaultConn.raddr = addr
|
||||
}
|
||||
|
||||
// 添加一个 特定的DNS服务器 , name为该dns服务器的名称. 若第一次调用, 则会设为 dm.DefaultConn
|
||||
func (dm *DNSMachine) AddNewServer(name string, addr *Addr) error {
|
||||
|
||||
if dm.defaultConn.Conn == nil { //若未配置过 DefaultConn
|
||||
dm.defaultConn = DnsConn{Conn: new(dns.Conn), raddr: addr, Name: name}
|
||||
err := dm.defaultConn.Dial()
|
||||
if err != nil {
|
||||
if ce := utils.CanLogErr("NewDnsMachine"); ce != nil {
|
||||
ce.Write(zap.Error(err))
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
dcc := &DnsConn{Conn: new(dns.Conn), raddr: addr, Name: name}
|
||||
err := dcc.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dc := new(dns.Conn)
|
||||
dc.Conn = conn
|
||||
dm.DefaultConn.Conn = dc
|
||||
}
|
||||
|
||||
return &dm
|
||||
}
|
||||
|
||||
func (dm *DNSMachine) SetDefaultConn(c net.Conn) {
|
||||
dm.DefaultConn.Conn = new(dns.Conn)
|
||||
dm.DefaultConn.Conn.Conn = c
|
||||
}
|
||||
|
||||
// 添加一个 特定名称的 域名服务器的 连接。
|
||||
//name为该dns服务器的名称
|
||||
func (dm *DNSMachine) AddConnForServer(name string, c net.Conn) {
|
||||
dc := new(dns.Conn)
|
||||
dc.Conn = c
|
||||
if dm.conns == nil {
|
||||
dm.conns = map[string]*DnsConn{}
|
||||
dm.conns = make(map[string]*DnsConn)
|
||||
}
|
||||
dcc := &DnsConn{Conn: dc}
|
||||
dm.conns[name] = dcc
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DNSMachine) Query(domain string) (ip net.IP) {
|
||||
switch dm.TypeStrategy {
|
||||
default:
|
||||
fallthrough
|
||||
case 0:
|
||||
fallthrough
|
||||
case 4:
|
||||
case 0, 4:
|
||||
ip = dm.QueryType(domain, dns.TypeA)
|
||||
if ip == nil {
|
||||
ip = dm.QueryType(domain, dns.TypeAAAA)
|
||||
@@ -187,14 +253,44 @@ func (dm *DNSMachine) Query(domain string) (ip net.IP) {
|
||||
//传入的domain必须是不带尾缀点号的domain, 即没有包过 Fqdn
|
||||
func (dm *DNSMachine) QueryType(domain string, dns_type uint16) (ip net.IP) {
|
||||
var generalCacheHit bool // 若读到了 cache 或 SpecialIPPollicy 的项, 则 generalCacheHit 为 true
|
||||
|
||||
var theDNSServerConn *DnsConn
|
||||
|
||||
defer func() {
|
||||
if theDNSServerConn != nil && theDNSServerConn.garbageMark {
|
||||
dm.mutex.Lock()
|
||||
delete(dm.conns, theDNSServerConn.Name)
|
||||
if theDNSServerConn == &dm.defaultConn {
|
||||
//如果DefaultConn都废了,那就糟糕
|
||||
//我们选一个备用的conn,升格为defaultConn
|
||||
|
||||
dm.defaultConn.Conn = nil
|
||||
|
||||
if len(dm.conns) > 0 {
|
||||
for name, c := range dm.conns {
|
||||
dm.defaultConn.Conn = c.Conn
|
||||
dm.defaultConn.garbageMark = false
|
||||
delete(dm.conns, name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
dm.mutex.Unlock()
|
||||
}
|
||||
|
||||
if generalCacheHit {
|
||||
|
||||
if ce := utils.CanLogDebug("[DNSMachine] hit cache"); ce != nil {
|
||||
ce.Write(zap.String("domain", domain), zap.String("ip", ip.String()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(ip) > 0 {
|
||||
if ce := utils.CanLogDebug("will add to dns cache"); ce != nil {
|
||||
ce.Write(zap.String("domain", domain))
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
if ce := utils.CanLogDebug("[DNSMachine] will add to cache"); ce != nil {
|
||||
ce.Write(zap.String("domain", domain), zap.String("ip", ip.String()))
|
||||
}
|
||||
|
||||
dm.mutex.Lock()
|
||||
@@ -202,15 +298,19 @@ func (dm *DNSMachine) QueryType(domain string, dns_type uint16) (ip net.IP) {
|
||||
|
||||
dm.cache = make(map[string]net.IP)
|
||||
}
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
|
||||
dm.cache[domain] = ip
|
||||
dm.mutex.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
//golang的defer原理: 多个defer 调用顺序是LIFO(后入先出),defer后的操作可以理解为压入栈中
|
||||
//所以实际是会先 dm.mutex.RUnlock(), 再调用 上面的 RLock, RUnlock, 所以不存在死锁导致无法return的问题
|
||||
|
||||
dm.mutex.RLock()
|
||||
defer dm.mutex.RUnlock()
|
||||
|
||||
// 查找步骤:
|
||||
//先从 cache找,有就直接返回
|
||||
//然后,
|
||||
//先查 specialIPPollicy,类似cache,有就直接返回
|
||||
@@ -221,9 +321,7 @@ func (dm *DNSMachine) QueryType(domain string, dns_type uint16) (ip net.IP) {
|
||||
if dm.cache != nil {
|
||||
if ip = dm.cache[domain]; ip != nil {
|
||||
generalCacheHit = true
|
||||
if ce := utils.CanLogDebug("hit dns cache"); ce != nil {
|
||||
ce.Write(zap.String("domain", domain))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -254,17 +352,49 @@ func (dm *DNSMachine) QueryType(domain string, dns_type uint16) (ip net.IP) {
|
||||
}
|
||||
}
|
||||
|
||||
theDNSServerConn := &dm.DefaultConn
|
||||
if dm.conns != nil && dm.SpecialServerPollicy != nil {
|
||||
theDNSServerConn = &dm.defaultConn
|
||||
if len(dm.conns) > 0 && len(dm.SpecialServerPollicy) > 0 {
|
||||
|
||||
if sn := dm.SpecialServerPollicy[domain]; sn != "" {
|
||||
if dnsServerName := dm.SpecialServerPollicy[domain]; dnsServerName != "" {
|
||||
|
||||
if serConn := dm.conns[domain]; serConn != nil {
|
||||
if serConn := dm.conns[dnsServerName]; serConn != nil {
|
||||
theDNSServerConn = serConn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if theDNSServerConn.Conn == nil { //如果配置文件只配置了自定义映射, 而没配置dns服务器的话, 那么我们就无法进行实际的dns查询
|
||||
if ce := utils.CanLogDebug("[DNSMachine] no server configured, return nil."); ce != nil {
|
||||
ce.Write()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
domain = dns.Fqdn(domain)
|
||||
return DNSQuery(domain, dns_type, theDNSServerConn.Conn, &theDNSServerConn.mutex, 0)
|
||||
|
||||
if ce := utils.CanLogDebug("[DNSMachine] start querying"); ce != nil {
|
||||
ce.Write(zap.String("domain", domain), zap.String("through", theDNSServerConn.Name))
|
||||
}
|
||||
|
||||
ip, err := DNSQuery(domain, dns_type, theDNSServerConn.Conn, &theDNSServerConn.mutex, 0)
|
||||
if Is_DNSQuery_returnType_ReadFatalErr(err) {
|
||||
//如果是读取的、非timeout的错误,那么我们直接认为底层连接出故障了, 我们需要重新dial
|
||||
//因为 miekg/dns 包会设置4秒的timeout,所以确实要筛除timeout的情况
|
||||
|
||||
theDNSServerConn.Conn.Close()
|
||||
err = theDNSServerConn.Dial()
|
||||
if err != nil {
|
||||
//再dial还是错误?那么就废了,
|
||||
if ce := utils.CanLogErr("[DNSMachine] Re-Dial Dns Server Failed"); ce != nil {
|
||||
ce.Write(zap.Error(err))
|
||||
}
|
||||
|
||||
theDNSServerConn.garbageMark = true
|
||||
}
|
||||
|
||||
//我们只是重新Dial,并不再次查询,否则就又递归了
|
||||
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
143
netLayer/dns_conf.go
Normal file
143
netLayer/dns_conf.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package netLayer
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/hahahrfool/v2ray_simple/utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type DnsConf struct {
|
||||
Strategy int64 `toml:"strategy"` //0表示默认(和4含义相同), 4表示先查ip4后查ip6, 6表示先查6后查4; 40表示只查ipv4, 60 表示只查ipv6
|
||||
Hosts map[string]any `toml:"hosts"` //用于强制指定哪些域名会被解析为哪些具体的ip;可以为一个ip字符串,或者一个 []string 数组, 数组内可以是A,AAAA或CNAME
|
||||
Servers []any `toml:"servers"` //可以为一个地址url字符串,或者为 SpecialDnsServerConf; 如果第一个元素是url字符串形式,则此第一个元素将会被用作默认dns服务器
|
||||
}
|
||||
|
||||
type SpecialDnsServerConf struct {
|
||||
AddrUrlStr string `toml:"addr"` //必须为 udp://1.1.1.1:53 这种格式
|
||||
Domains []string `toml:"domain"` //指定哪些域名需要通过 该dns服务器进行查询
|
||||
}
|
||||
|
||||
func loadSpecialDnsServerConf_fromTomlUnmarshaledMap(m map[string]any) *SpecialDnsServerConf {
|
||||
addr := m["addr"]
|
||||
if addr == nil {
|
||||
return nil
|
||||
}
|
||||
addrStr, ok := addr.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
domains := m["domain"]
|
||||
if domains == nil {
|
||||
return nil
|
||||
}
|
||||
domainsAnySlice, ok := domains.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
domainsSlice := []string{}
|
||||
|
||||
for _, anyD := range domainsAnySlice {
|
||||
dstr, ok := anyD.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
domainsSlice = append(domainsSlice, dstr)
|
||||
}
|
||||
return &SpecialDnsServerConf{
|
||||
Domains: domainsSlice,
|
||||
AddrUrlStr: addrStr,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func LoadDnsMachine(conf *DnsConf) *DNSMachine {
|
||||
var dm = &DNSMachine{TypeStrategy: conf.Strategy}
|
||||
|
||||
var ok = false
|
||||
|
||||
if len(conf.Servers) > 0 {
|
||||
//log.Println("conf.Servers", conf.Servers)
|
||||
ok = true
|
||||
servers := conf.Servers
|
||||
|
||||
dm.SpecialServerPollicy = make(map[string]string)
|
||||
|
||||
for _, ser := range servers {
|
||||
switch server := ser.(type) {
|
||||
case string:
|
||||
ad, e := NewAddrByURL(server)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dm.AddNewServer(server, &ad)
|
||||
|
||||
case map[string]any:
|
||||
|
||||
realServer := loadSpecialDnsServerConf_fromTomlUnmarshaledMap(server)
|
||||
if realServer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(realServer.Domains) <= 0 { //既然是特殊dns服务器, 那么就必须指定哪些域名要使用该dns服务器进行查询
|
||||
continue
|
||||
}
|
||||
|
||||
addr, e := NewAddrByURL(realServer.AddrUrlStr)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := dm.AddNewServer(realServer.AddrUrlStr, &addr); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, thisdomain := range realServer.Domains {
|
||||
dm.SpecialServerPollicy[thisdomain] = realServer.AddrUrlStr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if conf.Hosts != nil {
|
||||
ok = true
|
||||
dm.SpecialIPPollicy = make(map[string][]netip.Addr)
|
||||
|
||||
for thishost, things := range conf.Hosts {
|
||||
|
||||
switch value := things.(type) {
|
||||
case string:
|
||||
ip := net.ParseIP(value)
|
||||
|
||||
ad, _ := netip.AddrFromSlice(ip)
|
||||
|
||||
dm.SpecialIPPollicy[thishost] = []netip.Addr{ad}
|
||||
|
||||
case []string:
|
||||
for _, str := range value {
|
||||
ad, err := NewAddrFromAny(str)
|
||||
if err != nil {
|
||||
if utils.ZapLogger != nil {
|
||||
utils.ZapLogger.Fatal("LoadDnsMachine loading host err", zap.Error(err))
|
||||
} else {
|
||||
log.Fatalf("LoadDnsMachine loading host err %s\n", err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
dm.SpecialIPPollicy[thishost] = append(dm.SpecialIPPollicy[thishost], ad.GetHashable().Addr())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return dm
|
||||
}
|
||||
@@ -3,38 +3,36 @@ package netLayer_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hahahrfool/v2ray_simple/proxy"
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/hahahrfool/v2ray_simple/netLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/utils"
|
||||
)
|
||||
|
||||
func TestDNS(t *testing.T) {
|
||||
type testConfStruct struct {
|
||||
DnsConf *netLayer.DnsConf `toml:"dns"`
|
||||
}
|
||||
|
||||
func testDns_withConf(t *testing.T, config string) {
|
||||
|
||||
utils.LogLevel = utils.Log_debug
|
||||
utils.InitLog()
|
||||
|
||||
config := `
|
||||
|
||||
[dns]
|
||||
servers = [
|
||||
"udp://114.114.114.114:53"
|
||||
]
|
||||
|
||||
[dns.hosts]
|
||||
config += `
|
||||
[dns.hosts]
|
||||
"www.myfake.com" = "11.22.33.44"
|
||||
`
|
||||
var c testConfStruct
|
||||
_, e := toml.Decode(config, &c)
|
||||
|
||||
|
||||
`
|
||||
|
||||
c, e := proxy.LoadTomlConfStr(config)
|
||||
if e != nil {
|
||||
t.Log(e)
|
||||
t.FailNow()
|
||||
}
|
||||
t.Log(c.DnsConf)
|
||||
|
||||
dm := proxy.LoadDnsMachine(c.DnsConf)
|
||||
dm := netLayer.LoadDnsMachine(c.DnsConf)
|
||||
|
||||
t.Log(&dm)
|
||||
t.Log(dm.DefaultConn.RemoteAddr().Network(), dm.DefaultConn.RemoteAddr())
|
||||
|
||||
//dm.TypeStrategy = 60
|
||||
|
||||
@@ -44,5 +42,54 @@ servers = [
|
||||
|
||||
t.Log("record for imgstat.baidu.com is ", dm.Query("imgstat.baidu.com"))
|
||||
t.Log("record for imgstat.n.shifen.com is ", dm.Query("imgstat.n.shifen.com"))
|
||||
}
|
||||
|
||||
func TestDNS(t *testing.T) {
|
||||
const config = `
|
||||
[dns]
|
||||
servers = [
|
||||
"udp://114.114.114.114:53"
|
||||
]
|
||||
`
|
||||
testDns_withConf(t, config)
|
||||
}
|
||||
|
||||
func TestDNS_DoT(t *testing.T) {
|
||||
const config = `
|
||||
[dns]
|
||||
servers = [
|
||||
"tls://223.5.5.5:853"
|
||||
]
|
||||
`
|
||||
testDns_withConf(t, config)
|
||||
|
||||
}
|
||||
|
||||
func TestDNS_SpecialServer(t *testing.T) {
|
||||
const config = `
|
||||
[dns]
|
||||
servers = [
|
||||
{ addr = "udp://8.8.8.8:53", domain = [ "google.com" ] }
|
||||
]
|
||||
`
|
||||
utils.LogLevel = utils.Log_debug
|
||||
utils.InitLog()
|
||||
|
||||
var c testConfStruct
|
||||
_, e := toml.Decode(config, &c)
|
||||
|
||||
if e != nil {
|
||||
t.Log(e)
|
||||
t.FailNow()
|
||||
}
|
||||
t.Log(c.DnsConf)
|
||||
|
||||
dm := netLayer.LoadDnsMachine(c.DnsConf)
|
||||
|
||||
t.Log(&dm)
|
||||
|
||||
//dm.TypeStrategy = 60
|
||||
|
||||
t.Log("record for google.com is ", dm.Query("google.com"))
|
||||
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ func GetRawConn(reader io.Reader) syscall.RawConn {
|
||||
return nil
|
||||
}
|
||||
|
||||
//"udp", "udp4", "udp6"
|
||||
func IsStrUDP_network(s string) bool {
|
||||
switch s {
|
||||
case "udp", "udp4", "udp6":
|
||||
|
||||
109
netLayer/route_conf.go
Normal file
109
netLayer/route_conf.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package netLayer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hahahrfool/v2ray_simple/utils"
|
||||
"github.com/yl2chen/cidranger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type RuleConf struct {
|
||||
DialTag any `toml:"dialTag"`
|
||||
|
||||
InTags []string `toml:"inTag"`
|
||||
|
||||
Countries []string `toml:"country"` // 如果类似 !CN, 则意味着专门匹配不为CN 的国家(目前还未实现)
|
||||
IPs []string `toml:"ip"`
|
||||
Domains []string `toml:"domain"`
|
||||
Network []string `toml:"network"`
|
||||
}
|
||||
|
||||
func LoadRulesForRoutePolicy(rules []*RuleConf, policy *RoutePolicy) {
|
||||
for _, rc := range rules {
|
||||
newrs := LoadRuleForRouteSet(rc)
|
||||
policy.List = append(policy.List, newrs)
|
||||
}
|
||||
}
|
||||
|
||||
func LoadRuleForRouteSet(rule *RuleConf) (rs *RouteSet) {
|
||||
if len(GeositeListMap) == 0 {
|
||||
err := LoadGeositeFiles()
|
||||
if err != nil {
|
||||
if ce := utils.CanLogWarn("LoadGeositeFiles err"); ce != nil {
|
||||
ce.Write(zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
rs = NewFullRouteSet()
|
||||
|
||||
switch value := rule.DialTag.(type) {
|
||||
case string:
|
||||
rs.OutTag = value
|
||||
case []string:
|
||||
rs.OutTags = value
|
||||
}
|
||||
|
||||
for _, c := range rule.Countries {
|
||||
rs.Countries[c] = true
|
||||
}
|
||||
|
||||
for _, d := range rule.Domains {
|
||||
colonIdx := strings.Index(d, ":")
|
||||
if colonIdx < 0 {
|
||||
rs.Match = append(rs.Match, d)
|
||||
|
||||
} else {
|
||||
switch d[:colonIdx] {
|
||||
case "geosite":
|
||||
if GeositeListMap != nil {
|
||||
rs.Geosites = append(rs.Geosites, d[colonIdx+1:])
|
||||
|
||||
}
|
||||
case "full":
|
||||
rs.Full[d[colonIdx+1:]] = true
|
||||
case "domain":
|
||||
rs.Domains[d[colonIdx+1:]] = true
|
||||
case "regexp":
|
||||
reg, err := regexp.Compile(d[colonIdx+1:])
|
||||
if err == nil {
|
||||
rs.Regex = append(rs.Regex, reg)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
for _, t := range rule.InTags {
|
||||
rs.InTags[t] = true
|
||||
}
|
||||
|
||||
//ip 过滤 需要 分辨 cidr 和普通ip
|
||||
|
||||
for _, ipStr := range rule.IPs {
|
||||
if strings.Contains(ipStr, "/") {
|
||||
if _, net, err := net.ParseCIDR(ipStr); err == nil {
|
||||
rs.NetRanger.Insert(cidranger.NewBasicRangerEntry(*net))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
na, e := netip.ParseAddr(ipStr)
|
||||
if e == nil {
|
||||
rs.IPs[na] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ns := range rule.Network {
|
||||
tp := StrToTransportProtocol(ns)
|
||||
rs.AllowedTransportLayerProtocols |= tp
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
88
proxy/config.go
Normal file
88
proxy/config.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// CommonConf 是标准配置中 Listen和Dial 都有的部分
|
||||
//如果新协议有其他新项,可以放入 Extra.
|
||||
type CommonConf struct {
|
||||
Tag string `toml:"tag"` //可选
|
||||
Protocol string `toml:"protocol"` //约定,如果一个Protocol尾缀去掉了's'后仍然是一个有效协议,则该协议使用了 tls。这种方法继承自 v2simple,适合极简模式
|
||||
Uuid string `toml:"uuid"` //一个用户的唯一标识,建议使用uuid,但也不一定
|
||||
Host string `toml:"host"` //ip 或域名. 若unix domain socket 则为文件路径
|
||||
IP string `toml:"ip"` //给出Host后,该项可以省略; 既有Host又有ip的情况比较适合cdn
|
||||
Port int `toml:"port"` //若Network不为 unix , 则port项必填
|
||||
Version int `toml:"version"` //可选
|
||||
TLS bool `toml:"tls"` //可选. 如果不使用 's' 后缀法,则还可以配置这一项来更清晰第标明使用tls
|
||||
Insecure bool `toml:"insecure"` //tls 是否安全
|
||||
Alpn []string `toml:"alpn"`
|
||||
|
||||
Network string `toml:"network"` //默认使用tcp, network可选值为 tcp, udp, unix;
|
||||
|
||||
AdvancedLayer string `toml:"advancedLayer"` //可不填,或者为ws,或者为grpc
|
||||
|
||||
Path string `toml:"path"` //ws 的path 或 grpc的 serviceName。为了简便我们在同一位置给出.
|
||||
|
||||
Extra map[string]interface{} `toml:"extra"` //用于包含任意其它数据.虽然本包自己定义的协议肯定都是已知的,但是如果其他人使用了本包的话,那就有可能添加一些 新协议 特定的数据.
|
||||
}
|
||||
|
||||
func (cc *CommonConf) GetAddrStr() string {
|
||||
switch cc.Network {
|
||||
case "unix":
|
||||
return cc.Host
|
||||
|
||||
default:
|
||||
if cc.Host != "" {
|
||||
|
||||
return cc.Host + ":" + strconv.Itoa(cc.Port)
|
||||
} else {
|
||||
return cc.IP + ":" + strconv.Itoa(cc.Port)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//和 GetAddr的区别是,它优先使用ip,其次再使用host
|
||||
func (cc *CommonConf) GetAddrStrForListenOrDial() string {
|
||||
switch cc.Network {
|
||||
case "unix":
|
||||
return cc.Host
|
||||
|
||||
default:
|
||||
if cc.IP != "" {
|
||||
return cc.IP + ":" + strconv.Itoa(cc.Port)
|
||||
|
||||
} else {
|
||||
return cc.Host + ":" + strconv.Itoa(cc.Port)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 监听所使用的设置, 使用者可被称为 listener 或者 inServer
|
||||
// CommonConf.Host , CommonConf.IP, CommonConf.Port 为监听地址与端口
|
||||
type ListenConf struct {
|
||||
CommonConf
|
||||
Fallback any `toml:"fallback"` //可选,默认回落的地址,一般可以是ip:port,数字port 或者 unix socket的文件名
|
||||
TLSCert string `toml:"cert"`
|
||||
TLSKey string `toml:"key"`
|
||||
|
||||
//noroute 意味着 传入的数据 不会被分流,一定会被转发到默认的 dial
|
||||
// 这一项是针对 分流功能的. 如果不设noroute, 则所有listen 得到的流量都会被 试图 进行分流
|
||||
NoRoute bool `toml:"noroute"`
|
||||
|
||||
TargetAddr string `toml:"target"` //若使用dokodemo协议,则这一项会给出. 格式为url, 如 tcp://127.0.0.1:443 , 必须带scheme,以及端口。只能为tcp或udp
|
||||
|
||||
}
|
||||
|
||||
// 拨号所使用的设置, 使用者可被称为 dialer 或者 outClient
|
||||
// CommonConf.Host , CommonConf.IP, CommonConf.Port 为拨号地址与端口
|
||||
type DialConf struct {
|
||||
CommonConf
|
||||
Utls bool `toml:"utls"`
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/hahahrfool/v2ray_simple/httpLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/netLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/utils"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
type Simple struct {
|
||||
Server_ThatListenPort_Url string `json:"listen"`
|
||||
Client_ThatDialRemote_Url string `json:"dial"`
|
||||
Route []*RuleConf `json:"route" toml:"route"`
|
||||
Route []*netLayer.RuleConf `json:"route" toml:"route"`
|
||||
Fallbacks []*httpLayer.FallbackConf `json:"fallbacks"`
|
||||
MyCountryISO_3166 string `toml:"mycountry" json:"mycountry"`
|
||||
}
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/hahahrfool/v2ray_simple/httpLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/netLayer"
|
||||
"github.com/hahahrfool/v2ray_simple/utils"
|
||||
"github.com/yl2chen/cidranger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CommonConf 是标准配置中 Listen和Dial 都有的部分
|
||||
//如果新协议有其他新项,可以放入 Extra.
|
||||
type CommonConf struct {
|
||||
Tag string `toml:"tag"` //可选
|
||||
Protocol string `toml:"protocol"` //约定,如果一个Protocol尾缀去掉了's'后仍然是一个有效协议,则该协议使用了 tls。这种方法继承自 v2simple,适合极简模式
|
||||
Uuid string `toml:"uuid"` //一个用户的唯一标识,建议使用uuid,但也不一定
|
||||
Host string `toml:"host"` //ip 或域名. 若unix domain socket 则为文件路径
|
||||
IP string `toml:"ip"` //给出Host后,该项可以省略; 既有Host又有ip的情况比较适合cdn
|
||||
Port int `toml:"port"` //若Network不为 unix , 则port项必填
|
||||
Version int `toml:"version"` //可选
|
||||
TLS bool `toml:"tls"` //可选. 如果不使用 's' 后缀法,则还可以配置这一项来更清晰第标明使用tls
|
||||
Insecure bool `toml:"insecure"` //tls 是否安全
|
||||
Alpn []string `toml:"alpn"`
|
||||
|
||||
Network string `toml:"network"` //默认使用tcp, network可选值为 tcp, udp, unix;
|
||||
|
||||
AdvancedLayer string `toml:"advancedLayer"` //可不填,或者为ws,或者为grpc
|
||||
|
||||
Path string `toml:"path"` //ws 的path 或 grpc的 serviceName。为了简便我们在同一位置给出.
|
||||
|
||||
Extra map[string]interface{} `toml:"extra"` //用于包含任意其它数据.虽然本包自己定义的协议肯定都是已知的,但是如果其他人使用了本包的话,那就有可能添加一些 新协议 特定的数据.
|
||||
}
|
||||
|
||||
func (cc *CommonConf) GetAddrStr() string {
|
||||
switch cc.Network {
|
||||
case "unix":
|
||||
return cc.Host
|
||||
|
||||
default:
|
||||
if cc.Host != "" {
|
||||
|
||||
return cc.Host + ":" + strconv.Itoa(cc.Port)
|
||||
} else {
|
||||
return cc.IP + ":" + strconv.Itoa(cc.Port)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//和 GetAddr的区别是,它优先使用ip,其次再使用host
|
||||
func (cc *CommonConf) GetAddrStrForListenOrDial() string {
|
||||
switch cc.Network {
|
||||
case "unix":
|
||||
return cc.Host
|
||||
|
||||
default:
|
||||
if cc.IP != "" {
|
||||
return cc.IP + ":" + strconv.Itoa(cc.Port)
|
||||
|
||||
} else {
|
||||
return cc.Host + ":" + strconv.Itoa(cc.Port)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 监听所使用的设置, 使用者可被称为 listener 或者 inServer
|
||||
// CommonConf.Host , CommonConf.IP, CommonConf.Port 为监听地址与端口
|
||||
type ListenConf struct {
|
||||
CommonConf
|
||||
Fallback any `toml:"fallback"` //可选,默认回落的地址,一般可以是ip:port,数字port 或者 unix socket的文件名
|
||||
TLSCert string `toml:"cert"`
|
||||
TLSKey string `toml:"key"`
|
||||
|
||||
//noroute 意味着 传入的数据 不会被分流,一定会被转发到默认的 dial
|
||||
// 这一项是针对 mycountry 分流功能的. 如果不设noroute, 且给定了 app.mycountry, 则所有listener 得到的流量都会被 试图 进行国别分流
|
||||
NoRoute bool `toml:"noroute"`
|
||||
|
||||
TargetAddr string `toml:"target"` //若使用dokodemo协议,则这一项会给出. 格式 tcp://127.0.0.1:443 , 必须带scheme,以及端口。只能为tcp或udp
|
||||
|
||||
}
|
||||
|
||||
// 拨号所使用的设置, 使用者可被称为 dialer 或者 outClient
|
||||
// CommonConf.Host , CommonConf.IP, CommonConf.Port 为拨号地址与端口
|
||||
type DialConf struct {
|
||||
CommonConf
|
||||
Utls bool `toml:"utls"`
|
||||
}
|
||||
|
||||
type AppConf struct {
|
||||
LogLevel *int `toml:"loglevel"` //需要为指针, 否则无法判断0到底是未给出的默认值还是 显式声明的0
|
||||
DefaultUUID string `toml:"default_uuid"`
|
||||
MyCountryISO_3166 string `toml:"mycountry" json:"mycountry"` //加了mycountry后,就会自动按照geoip分流,也会对顶级域名进行国别分流
|
||||
|
||||
NoReadV bool `toml:"noreadv"`
|
||||
|
||||
AdminPass string `toml:"admin_pass"`
|
||||
}
|
||||
|
||||
type DnsConf struct {
|
||||
Strategy int64 `toml:"strategy"` //0表示默认(和4含义相同), 4表示先查ip4后查ip6, 6表示先查6后查4; 40表示只查ipv4, 60 表示只查ipv6
|
||||
Hosts map[string]any `toml:"hosts"` //用于强制指定哪些域名会被解析为哪些具体的ip;可以为一个ip字符串,或者一个 []string 数组, 数组内可以是A,AAAA或CNAME
|
||||
Servers []any `toml:"servers"` //可以为一个地址url字符串,或者为 SpecialDnsServerConf; 如果第一个元素是字符串形式,则此第一个元素将会被用作默认dns服务器
|
||||
}
|
||||
|
||||
type SpecialDnsServerConf struct {
|
||||
Addr string `toml:"addr"` //必须为 udp://1.1.1.1:53 这种格式
|
||||
Domains []string `toml:"domains"` //指定哪些域名需要通过 该dns服务器进行查询
|
||||
}
|
||||
|
||||
type RuleConf struct {
|
||||
DialTag any `toml:"dialTag"`
|
||||
|
||||
InTags []string `toml:"inTag"`
|
||||
|
||||
Countries []string `toml:"country"` // 如果类似 !CN, 则意味着专门匹配不为CN 的国家(目前还未实现)
|
||||
IPs []string `toml:"ip"`
|
||||
Domains []string `toml:"domain"`
|
||||
Network []string `toml:"network"`
|
||||
}
|
||||
|
||||
//标准配置。默认使用toml格式
|
||||
// toml:https://toml.io/cn/
|
||||
// english: https://toml.io/en/
|
||||
type Standard struct {
|
||||
App *AppConf `toml:"app"`
|
||||
DnsConf *DnsConf `toml:"dns"`
|
||||
|
||||
Listen []*ListenConf `toml:"listen"`
|
||||
Dial []*DialConf `toml:"dial"`
|
||||
|
||||
Route []*RuleConf `toml:"route"`
|
||||
Fallbacks []*httpLayer.FallbackConf `toml:"fallback"`
|
||||
}
|
||||
|
||||
func LoadTomlConfStr(str string) (c Standard, err error) {
|
||||
_, err = toml.Decode(str, &c)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func LoadTomlConfFile(fileNamePath string) (Standard, error) {
|
||||
|
||||
if cf, err := os.Open(fileNamePath); err == nil {
|
||||
defer cf.Close()
|
||||
bs, _ := ioutil.ReadAll(cf)
|
||||
return LoadTomlConfStr(string(bs))
|
||||
} else {
|
||||
return Standard{}, utils.ErrInErr{ErrDesc: "can't open config file", ErrDetail: err}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func LoadRulesForRoutePolicy(rules []*RuleConf, policy *netLayer.RoutePolicy) {
|
||||
for _, rc := range rules {
|
||||
newrs := LoadRuleForRouteSet(rc)
|
||||
policy.List = append(policy.List, newrs)
|
||||
}
|
||||
}
|
||||
|
||||
func LoadRuleForRouteSet(rule *RuleConf) (rs *netLayer.RouteSet) {
|
||||
if len(netLayer.GeositeListMap) == 0 {
|
||||
err := netLayer.LoadGeositeFiles()
|
||||
if err != nil {
|
||||
if ce := utils.CanLogWarn("netLayer.LoadGeositeFiles err"); ce != nil {
|
||||
ce.Write(zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
rs = netLayer.NewFullRouteSet()
|
||||
|
||||
switch value := rule.DialTag.(type) {
|
||||
case string:
|
||||
rs.OutTag = value
|
||||
case []string:
|
||||
rs.OutTags = value
|
||||
}
|
||||
|
||||
for _, c := range rule.Countries {
|
||||
rs.Countries[c] = true
|
||||
}
|
||||
|
||||
for _, d := range rule.Domains {
|
||||
colonIdx := strings.Index(d, ":")
|
||||
if colonIdx < 0 {
|
||||
rs.Match = append(rs.Match, d)
|
||||
|
||||
} else {
|
||||
switch d[:colonIdx] {
|
||||
case "geosite":
|
||||
if netLayer.GeositeListMap != nil {
|
||||
rs.Geosites = append(rs.Geosites, d[colonIdx+1:])
|
||||
|
||||
}
|
||||
case "full":
|
||||
rs.Full[d[colonIdx+1:]] = true
|
||||
case "domain":
|
||||
rs.Domains[d[colonIdx+1:]] = true
|
||||
case "regexp":
|
||||
reg, err := regexp.Compile(d[colonIdx+1:])
|
||||
if err == nil {
|
||||
rs.Regex = append(rs.Regex, reg)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
for _, t := range rule.InTags {
|
||||
rs.InTags[t] = true
|
||||
}
|
||||
|
||||
//ip 过滤 需要 分辨 cidr 和普通ip
|
||||
|
||||
for _, ipStr := range rule.IPs {
|
||||
if strings.Contains(ipStr, "/") {
|
||||
if _, net, err := net.ParseCIDR(ipStr); err == nil {
|
||||
rs.NetRanger.Insert(cidranger.NewBasicRangerEntry(*net))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
na, e := netip.ParseAddr(ipStr)
|
||||
if e == nil {
|
||||
rs.IPs[na] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ns := range rule.Network {
|
||||
tp := netLayer.StrToTransportProtocol(ns)
|
||||
rs.AllowedTransportLayerProtocols |= tp
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func LoadDnsMachine(conf *DnsConf) *netLayer.DNSMachine {
|
||||
var dm = &netLayer.DNSMachine{TypeStrategy: conf.Strategy}
|
||||
|
||||
var ok = false
|
||||
|
||||
if len(conf.Servers) > 0 {
|
||||
ok = true
|
||||
ss := conf.Servers
|
||||
first := ss[0]
|
||||
firstDealed := false
|
||||
|
||||
switch value := first.(type) {
|
||||
case string:
|
||||
ad, e := netLayer.NewAddrByURL(value)
|
||||
if e != nil {
|
||||
if utils.ZapLogger != nil {
|
||||
utils.ZapLogger.Fatal("LoadDnsMachine loading server err", zap.Error(e))
|
||||
} else {
|
||||
log.Fatalf("LoadDnsMachine loading server err %s\n", e)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
dm = netLayer.NewDnsMachine(&ad)
|
||||
dm.TypeStrategy = conf.Strategy
|
||||
firstDealed = true
|
||||
}
|
||||
|
||||
if firstDealed {
|
||||
ss = ss[1:]
|
||||
}
|
||||
|
||||
dm.SpecialServerPollicy = make(map[string]string)
|
||||
|
||||
for _, s := range ss {
|
||||
switch value := s.(type) {
|
||||
case SpecialDnsServerConf:
|
||||
|
||||
for _, d := range value.Domains {
|
||||
dm.SpecialServerPollicy[d] = value.Addr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if conf.Hosts != nil {
|
||||
ok = true
|
||||
dm.SpecialIPPollicy = make(map[string][]netip.Addr)
|
||||
|
||||
for thishost, things := range conf.Hosts {
|
||||
|
||||
switch value := things.(type) {
|
||||
case string:
|
||||
ip := net.ParseIP(value)
|
||||
|
||||
ad, _ := netip.AddrFromSlice(ip)
|
||||
|
||||
dm.SpecialIPPollicy[thishost] = []netip.Addr{ad}
|
||||
|
||||
case []string:
|
||||
for _, str := range value {
|
||||
ad, err := netLayer.NewAddrFromAny(str)
|
||||
if err != nil {
|
||||
if utils.ZapLogger != nil {
|
||||
utils.ZapLogger.Fatal("LoadDnsMachine loading host err", zap.Error(err))
|
||||
} else {
|
||||
log.Fatalf("LoadDnsMachine loading host err %s\n", err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
dm.SpecialIPPollicy[thishost] = append(dm.SpecialIPPollicy[thishost], ad.GetHashable().Addr())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return dm
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/hahahrfool/v2ray_simple/proxy"
|
||||
)
|
||||
|
||||
@@ -48,61 +47,3 @@ func TestClientSimpleConfig(t *testing.T) {
|
||||
t.Log(i, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTomlConfig(t *testing.T) {
|
||||
|
||||
var conf proxy.Standard
|
||||
_, err := toml.Decode(testTomlConfStr, &conf)
|
||||
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
t.Log(conf)
|
||||
t.Log("dial0", conf.Dial[0])
|
||||
t.Log("listen0", conf.Listen[0])
|
||||
t.Log("extra", conf.Listen[0].Extra)
|
||||
t.Log(conf.Route[0])
|
||||
t.Log(conf.Route[1])
|
||||
t.Log(conf.Fallbacks)
|
||||
}
|
||||
|
||||
const testTomlConfStr = `# this is a verysimple standard config
|
||||
|
||||
[app]
|
||||
mycountry = "CN"
|
||||
|
||||
[[dial]]
|
||||
tag = "my_vlesss1"
|
||||
protocol = "vlesss"
|
||||
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
|
||||
host = "127.0.0.1"
|
||||
port = 4433
|
||||
version = 0
|
||||
insecure = true
|
||||
utls = true
|
||||
|
||||
[[listen]]
|
||||
protocol = "socks5"
|
||||
host = "127.0.0.1"
|
||||
port = 1080
|
||||
tag = "my_socks51"
|
||||
extra = { ws_earlydata = 4096 }
|
||||
|
||||
|
||||
[[route]]
|
||||
dialTag = "my_ws1"
|
||||
country = ["CN"]
|
||||
ip = ["0.0.0.0/8","10.0.0.0/8","fe80::/10","10.0.0.1"]
|
||||
domain = ["www.google.com","www.twitter.com"]
|
||||
network = ["tcp","udp"]
|
||||
|
||||
[[route]]
|
||||
dialTag = "my_vless1"
|
||||
|
||||
|
||||
[[fallback]]
|
||||
path = "/asf"
|
||||
dest = 6060
|
||||
`
|
||||
|
||||
63
test_test.go
63
test_test.go
@@ -4,6 +4,7 @@ import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/hahahrfool/v2ray_simple/proxy"
|
||||
"github.com/hahahrfool/v2ray_simple/utils"
|
||||
"github.com/miekg/dns"
|
||||
@@ -81,12 +82,12 @@ cert = "cert.pem"
|
||||
key = "cert.key"
|
||||
`
|
||||
|
||||
clientConf, err := proxy.LoadTomlConfStr(testClientConfStr)
|
||||
clientConf, err := LoadTomlConfStr(testClientConfStr)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
}
|
||||
serverConf, err := proxy.LoadTomlConfStr(testServerConfStr)
|
||||
serverConf, err := LoadTomlConfStr(testServerConfStr)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
@@ -142,3 +143,61 @@ key = "cert.key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTomlConf(t *testing.T) {
|
||||
|
||||
var conf StandardConf
|
||||
_, err := toml.Decode(testTomlConfStr, &conf)
|
||||
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
t.Log(conf)
|
||||
t.Log("dial0", conf.Dial[0])
|
||||
t.Log("listen0", conf.Listen[0])
|
||||
t.Log("extra", conf.Listen[0].Extra)
|
||||
t.Log(conf.Route[0])
|
||||
t.Log(conf.Route[1])
|
||||
t.Log(conf.Fallbacks)
|
||||
}
|
||||
|
||||
const testTomlConfStr = `# this is a verysimple standard config
|
||||
|
||||
[app]
|
||||
mycountry = "CN"
|
||||
|
||||
[[dial]]
|
||||
tag = "my_vlesss1"
|
||||
protocol = "vlesss"
|
||||
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
|
||||
host = "127.0.0.1"
|
||||
port = 4433
|
||||
version = 0
|
||||
insecure = true
|
||||
utls = true
|
||||
|
||||
[[listen]]
|
||||
protocol = "socks5"
|
||||
host = "127.0.0.1"
|
||||
port = 1080
|
||||
tag = "my_socks51"
|
||||
extra = { ws_earlydata = 4096 }
|
||||
|
||||
|
||||
[[route]]
|
||||
dialTag = "my_ws1"
|
||||
country = ["CN"]
|
||||
ip = ["0.0.0.0/8","10.0.0.0/8","fe80::/10","10.0.0.1"]
|
||||
domain = ["www.google.com","www.twitter.com"]
|
||||
network = ["tcp","udp"]
|
||||
|
||||
[[route]]
|
||||
dialTag = "my_vless1"
|
||||
|
||||
|
||||
[[fallback]]
|
||||
path = "/asf"
|
||||
dest = 6060
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user