mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 19:14:01 +03:00
Merge remote-tracking branch 'origin/master' into stable
This commit is contained in:
@@ -24,3 +24,28 @@ type Conn interface {
|
|||||||
CloseableReader
|
CloseableReader
|
||||||
CloseableWriter
|
CloseableWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type netConnWrapper struct {
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n netConnWrapper) CloseRead() error {
|
||||||
|
if conn, ok := n.Conn.(CloseableReader); ok {
|
||||||
|
return conn.CloseRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n netConnWrapper) CloseWrite() error {
|
||||||
|
if conn, ok := n.Conn.(CloseableWriter); ok {
|
||||||
|
return conn.CloseWrite()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapConn wraps a generic [net.Conn] into Conn.
|
||||||
|
func WrapNetConn(conn net.Conn) Conn {
|
||||||
|
return netConnWrapper{conn}
|
||||||
|
}
|
||||||
|
|||||||
+27
-14
@@ -134,8 +134,33 @@ allow-fallback-on-unknown-dc = false
|
|||||||
# it has to access.
|
# it has to access.
|
||||||
#
|
#
|
||||||
# By default we use Cloudflare.
|
# By default we use Cloudflare.
|
||||||
|
#
|
||||||
|
# DEPRECATED option:
|
||||||
|
# If dns option is specified, it will be used instead
|
||||||
doh-ip = "1.1.1.1"
|
doh-ip = "1.1.1.1"
|
||||||
|
|
||||||
|
# Starting from mtg v2.1.12 we have changed a configuration for DNS. Now it
|
||||||
|
# supports DNS-over-HTTPS, DNS-over-TLS, custom UDP resolver and system
|
||||||
|
# resolver.
|
||||||
|
#
|
||||||
|
# Here is how to define DNS-over-HTTPS:
|
||||||
|
# - https://1.1.1.1
|
||||||
|
# - https://1.1.1.1/dns-query
|
||||||
|
# - https://cloudflare-dns.com/dns-query
|
||||||
|
# - https://cloudflare-dns.com
|
||||||
|
#
|
||||||
|
# Here is how to define DNS-over-TLS:
|
||||||
|
# - tls://1.1.1.1
|
||||||
|
# - tls://cloudflare-dns.com
|
||||||
|
#
|
||||||
|
# Here is how to define a custom UDP resolver (we support only IPs here)
|
||||||
|
# - 1.1.1.1
|
||||||
|
# - udp://1.1.1.1
|
||||||
|
#
|
||||||
|
# If you set it to empty string, default resolver will be used.
|
||||||
|
# But please comment out doh-ip
|
||||||
|
dns = "https://1.1.1.1"
|
||||||
|
|
||||||
# mtg can work via proxies (for now, we support only socks5). Proxy
|
# mtg can work via proxies (for now, we support only socks5). Proxy
|
||||||
# configuration is done via list. So, you can specify many proxies
|
# configuration is done via list. So, you can specify many proxies
|
||||||
# there.
|
# there.
|
||||||
@@ -149,25 +174,13 @@ doh-ip = "1.1.1.1"
|
|||||||
#
|
#
|
||||||
# Proxy configuration is done via ordinary URI schema:
|
# Proxy configuration is done via ordinary URI schema:
|
||||||
#
|
#
|
||||||
# socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s
|
# socks5://user:password@host:port
|
||||||
#
|
#
|
||||||
# Only socks5 proxy is used. user/password is optional. As you can
|
# Only socks5 proxy is used. user/password is optional. As you can
|
||||||
# see, you can specify some parameters in GET query. These parameters
|
# see, you can specify some parameters in GET query. These parameters
|
||||||
# configure circuit breaker.
|
# configure circuit breaker.
|
||||||
#
|
|
||||||
# open_threshold means a number of errors which should happen so we stop
|
|
||||||
# use a proxy.
|
|
||||||
#
|
|
||||||
# half_open_timeout means a time period (in Golang duration notation)
|
|
||||||
# after which we can retry with this proxy
|
|
||||||
#
|
|
||||||
# reset_failures_timeout means a time period when we flush out errors
|
|
||||||
# when circuit breaker in closed state.
|
|
||||||
#
|
|
||||||
# Please see https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
|
|
||||||
# on details about circuit breakers.
|
|
||||||
proxies = [
|
proxies = [
|
||||||
# "socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s"
|
# "socks5://user:password@host:port"
|
||||||
]
|
]
|
||||||
|
|
||||||
# network timeouts define different settings for timeouts. tcp timeout
|
# network timeouts define different settings for timeouts. tcp timeout
|
||||||
|
|||||||
@@ -21,14 +21,16 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
|
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.51.0
|
||||||
golang.org/x/sys v0.41.0
|
golang.org/x/sys v0.41.0
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ncruces/go-dns v1.3.2
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/pires/go-proxyproto v0.11.0
|
github.com/pires/go-proxyproto v0.11.0
|
||||||
|
github.com/things-go/go-socks5 v0.1.0
|
||||||
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e
|
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e
|
||||||
github.com/yl2chen/cidranger v1.0.2
|
github.com/yl2chen/cidranger v1.0.2
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
|||||||
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/ncruces/go-dns v1.3.2 h1:kBLuUZBgkQ4qF4WDXZRQ4rG0Gk6sLVJQ5tESkWrxUa0=
|
||||||
|
github.com/ncruces/go-dns v1.3.2/go.mod h1:tuzixNY8PY/M7yUzcvRbUaeLs3ifIdydpi5H2bfRU+s=
|
||||||
github.com/panjf2000/ants/v2 v2.11.5 h1:a7LMnMEeux/ebqTux140tRiaqcFTV0q2bEHF03nl6Rg=
|
github.com/panjf2000/ants/v2 v2.11.5 h1:a7LMnMEeux/ebqTux140tRiaqcFTV0q2bEHF03nl6Rg=
|
||||||
github.com/panjf2000/ants/v2 v2.11.5/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
github.com/panjf2000/ants/v2 v2.11.5/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
@@ -89,6 +91,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg=
|
||||||
|
github.com/things-go/go-socks5 v0.1.0/go.mod h1:Riabiyu52kLsla0YmJqunt1c1JEl6iXSr4bRd7swFEA=
|
||||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
|
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
|
||||||
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae h1:ArVM1jICfm7g4E4dBet+KHUFMLuxmj1Nxdp/tr3ByCU=
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae h1:ArVM1jICfm7g4E4dBet+KHUFMLuxmj1Nxdp/tr3ByCU=
|
||||||
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae/go.mod h1:cldYm15/XHcGt7ndItnEWHwFZo7dinU+2QoyjfErhsI=
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae/go.mod h1:cldYm15/XHcGt7ndItnEWHwFZo7dinU+2QoyjfErhsI=
|
||||||
@@ -115,8 +119,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|||||||
+25
-28
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/9seconds/mtg/v2/antireplay"
|
"github.com/9seconds/mtg/v2/antireplay"
|
||||||
@@ -16,7 +15,7 @@ import (
|
|||||||
"github.com/9seconds/mtg/v2/ipblocklist/files"
|
"github.com/9seconds/mtg/v2/ipblocklist/files"
|
||||||
"github.com/9seconds/mtg/v2/logger"
|
"github.com/9seconds/mtg/v2/logger"
|
||||||
"github.com/9seconds/mtg/v2/mtglib"
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
"github.com/9seconds/mtg/v2/network"
|
"github.com/9seconds/mtg/v2/network/v2"
|
||||||
"github.com/9seconds/mtg/v2/stats"
|
"github.com/9seconds/mtg/v2/stats"
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
@@ -40,43 +39,41 @@ func makeLogger(conf *config.Config) mtglib.Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
|
func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
|
||||||
tcpTimeout := conf.Network.Timeout.TCP.Get(network.DefaultTimeout)
|
resolver, err := network.GetDNS(conf.GetDNS())
|
||||||
httpTimeout := conf.Network.Timeout.HTTP.Get(network.DefaultHTTPTimeout)
|
|
||||||
dohIP := conf.Network.DOHIP.Get(net.ParseIP(network.DefaultDOHHostname)).String()
|
|
||||||
userAgent := "mtg/" + version
|
|
||||||
|
|
||||||
baseDialer, err := network.NewDefaultDialer(tcpTimeout, 0)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot build a default dialer: %w", err)
|
return nil, fmt.Errorf("cannot create DNS resolver: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(conf.Network.Proxies) == 0 {
|
base := network.New(
|
||||||
return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) //nolint: wrapcheck
|
resolver,
|
||||||
}
|
"mtg/"+version,
|
||||||
|
conf.Network.Timeout.TCP.Get(0),
|
||||||
|
conf.Network.Timeout.HTTP.Get(0),
|
||||||
|
conf.Network.Timeout.Idle.Get(0),
|
||||||
|
)
|
||||||
|
|
||||||
proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies))
|
proxyDialers := make([]network.Network, len(conf.Network.Proxies))
|
||||||
|
for idx, v := range conf.Network.Proxies {
|
||||||
for _, v := range conf.Network.Proxies {
|
value, err := network.NewProxyNetwork(base, v.Get(nil))
|
||||||
if value := v.Get(nil); value != nil {
|
|
||||||
proxyURLs = append(proxyURLs, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(proxyURLs) == 1 {
|
|
||||||
socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
|
return nil, fmt.Errorf("cannot use %v for proxy url: %w", v.Get(nil), err)
|
||||||
}
|
}
|
||||||
|
proxyDialers[idx] = value
|
||||||
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) //nolint: wrapcheck
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs)
|
switch len(proxyDialers) {
|
||||||
|
case 0:
|
||||||
|
return base, nil
|
||||||
|
case 1:
|
||||||
|
return proxyDialers[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := network.Join(proxyDialers...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) //nolint: wrapcheck
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache {
|
func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/9seconds/mtg/v2/mtglib"
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
)
|
)
|
||||||
@@ -56,6 +57,7 @@ type Config struct {
|
|||||||
Idle TypeDuration `json:"idle"`
|
Idle TypeDuration `json:"idle"`
|
||||||
} `json:"timeout"`
|
} `json:"timeout"`
|
||||||
DOHIP TypeIP `json:"dohIp"`
|
DOHIP TypeIP `json:"dohIp"`
|
||||||
|
DNS TypeDNSURI `json:"dns"`
|
||||||
Proxies []TypeProxyURL `json:"proxies"`
|
Proxies []TypeProxyURL `json:"proxies"`
|
||||||
} `json:"network"`
|
} `json:"network"`
|
||||||
Stats struct {
|
Stats struct {
|
||||||
@@ -76,6 +78,16 @@ type Config struct {
|
|||||||
} `json:"stats"`
|
} `json:"stats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetDNS() *url.URL {
|
||||||
|
var dohURL *url.URL
|
||||||
|
|
||||||
|
if dohIP := c.Network.DOHIP.Get(nil); dohIP != nil {
|
||||||
|
dohURL, _ = url.Parse("https://" + dohIP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Network.DNS.Get(dohURL)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) GetDomainFrontingPort(defaultValue uint) uint {
|
func (c *Config) GetDomainFrontingPort(defaultValue uint) uint {
|
||||||
if port := c.DomainFronting.Port.Get(0); port != 0 {
|
if port := c.DomainFronting.Port.Get(0); port != 0 {
|
||||||
return port
|
return port
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ type tomlConfig struct {
|
|||||||
Idle string `toml:"idle" json:"idle,omitempty"`
|
Idle string `toml:"idle" json:"idle,omitempty"`
|
||||||
} `toml:"timeout" json:"timeout,omitempty"`
|
} `toml:"timeout" json:"timeout,omitempty"`
|
||||||
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
|
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
|
||||||
|
DNS string `toml:"dns" json:"dns,omitempty"`
|
||||||
Proxies []string `toml:"proxies" json:"proxies,omitempty"`
|
Proxies []string `toml:"proxies" json:"proxies,omitempty"`
|
||||||
} `toml:"network" json:"network,omitempty"`
|
} `toml:"network" json:"network,omitempty"`
|
||||||
Stats struct {
|
Stats struct {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeDNSURI struct {
|
||||||
|
Value *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDNSURI) Set(value string) error {
|
||||||
|
parsed, err := url.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("value is not URI: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.Host == "" {
|
||||||
|
parsed.Host = parsed.Path
|
||||||
|
parsed.Path = ""
|
||||||
|
parsed.Scheme = "udp"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parsed.Scheme {
|
||||||
|
case "https", "tls":
|
||||||
|
case "udp":
|
||||||
|
if ip := net.ParseIP(parsed.Hostname()); ip == nil {
|
||||||
|
return fmt.Errorf("simple DNS must IP address: %s", parsed.Hostname())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported DNS type %s", parsed.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.Scheme != "https" && parsed.Path != "" {
|
||||||
|
return fmt.Errorf("path is supported only for DoH: %s", parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.User != nil {
|
||||||
|
return fmt.Errorf("used info is not supported: %s", parsed.User.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = parsed
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDNSURI) Get(defaultValue *url.URL) *url.URL {
|
||||||
|
if t.Value != nil {
|
||||||
|
return t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDNSURI) UnmarshalText(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDNSURI) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDNSURI) String() string {
|
||||||
|
if t.Value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Value.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeDNSURITestStruct struct {
|
||||||
|
Value config.TypeDNSURI `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeDNSURITestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []string{
|
||||||
|
"xx",
|
||||||
|
"ppar",
|
||||||
|
"",
|
||||||
|
"dns://hahaha",
|
||||||
|
"udp://xcxxcv",
|
||||||
|
"udp://1.1.1.1/xcv",
|
||||||
|
"1.1.1.1/xxx",
|
||||||
|
"tls://dns/xx",
|
||||||
|
"tls://1.1.1.1/xx",
|
||||||
|
"https://user:password@1.1.1.1",
|
||||||
|
"tls://user:password@1.1.1.1",
|
||||||
|
"udp://user:password@1.1.1.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
assert.Error(t, json.Unmarshal(data, &typeDNSURITestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestUnmarshalOk() {
|
||||||
|
testData := []string{
|
||||||
|
"1.1.1.1",
|
||||||
|
"tls://1.1.1.1",
|
||||||
|
"tls://dns.google",
|
||||||
|
"https://1.1.1.1",
|
||||||
|
"https://1.1.1.1/dns-query",
|
||||||
|
"https://dns.google",
|
||||||
|
"https://dns.google/dns-query",
|
||||||
|
"udp://1.1.1.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
testStruct := &typeDNSURITestStruct{}
|
||||||
|
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||||
|
if v == "1.1.1.1" {
|
||||||
|
v = "udp://" + v
|
||||||
|
}
|
||||||
|
assert.Equal(t, v, testStruct.Value.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestMarshalOk() {
|
||||||
|
testData := []string{
|
||||||
|
"tls://1.1.1.1",
|
||||||
|
"tls://dns.google",
|
||||||
|
"https://1.1.1.1",
|
||||||
|
"https://1.1.1.1/dns-query",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
testStruct := &typePreferIPTestStruct{
|
||||||
|
Value: config.TypePreferIP{
|
||||||
|
Value: v,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedJSON, err := json.Marshal(testStruct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expectedJSON, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestGet() {
|
||||||
|
value := config.TypeDNSURI{}
|
||||||
|
suite.Nil(value.Get(nil))
|
||||||
|
|
||||||
|
suite.NoError(value.Set("tls://1.1.1.1"))
|
||||||
|
suite.NotNil(value.Get(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSURI(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeDNSURITestSuite{})
|
||||||
|
}
|
||||||
@@ -15,20 +15,24 @@ type TypeProxyURL struct {
|
|||||||
func (t *TypeProxyURL) Set(value string) error {
|
func (t *TypeProxyURL) Set(value string) error {
|
||||||
parsedURL, err := url.Parse(value)
|
parsedURL, err := url.Parse(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("value is not corect URL (%s): %w", value, err)
|
return fmt.Errorf("value is not correct URL (%s): %w", value, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedURL.Host == "" {
|
if parsedURL.Host == "" {
|
||||||
return fmt.Errorf("url has to have a schema: %s", value)
|
return fmt.Errorf("url has to have a schema: %s", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedURL.Scheme != "socks5" {
|
switch parsedURL.Scheme {
|
||||||
|
case "socks5", "socks5h":
|
||||||
|
default:
|
||||||
return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
|
return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil {
|
if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil {
|
||||||
parsedURL.Host = net.JoinHostPort(parsedURL.Host,
|
parsedURL.Host = net.JoinHostPort(
|
||||||
typeProxyURLDefaultSOCKS5Port)
|
parsedURL.Host,
|
||||||
|
typeProxyURLDefaultSOCKS5Port,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Value = parsedURL
|
t.Value = parsedURL
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package network_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/network/v2"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseHTTPTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
http *httptest.Server
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseHTTPTestSuite) SetupSuite() {
|
||||||
|
suite.http = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(r.Header.Get("User-Agent"))) //nolint: errcheck
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseHTTPTestSuite) SetupTest() {
|
||||||
|
suite.client = network.New(nil, "mtg/1", 0, 0, 0).MakeHTTPClient(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseHTTPTestSuite) TestGet() {
|
||||||
|
resp, err := suite.client.Get(suite.http.URL)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
defer resp.Body.Close() //nolint: errcheck
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal("mtg/1", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseHTTPTestSuite) TearDownSuite() {
|
||||||
|
suite.http.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseHTTP(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &BaseHTTPTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package network_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/network/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseNetworkTestSuite struct {
|
||||||
|
EchoServerTestSuite
|
||||||
|
|
||||||
|
net network.Network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseNetworkTestSuite) SetupSuite() {
|
||||||
|
suite.EchoServerTestSuite.SetupSuite()
|
||||||
|
|
||||||
|
suite.net = network.New(nil, "agent", 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseNetworkTestSuite) TestDialUnknownNetwork() {
|
||||||
|
testData := []string{
|
||||||
|
"udp",
|
||||||
|
"udp4",
|
||||||
|
"udp6",
|
||||||
|
"unix",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range testData {
|
||||||
|
suite.T().Run(name, func(t *testing.T) {
|
||||||
|
_, err := suite.net.Dial(name, suite.EchoServerAddr())
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseNetworkTestSuite) TestDial() {
|
||||||
|
conn, err := suite.net.Dial("tcp4", suite.EchoServerAddr())
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
buf := []byte{1, 2, 3, 4, 5}
|
||||||
|
n, err := conn.Write(buf)
|
||||||
|
suite.Equal(5, n)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
another := make([]byte, len(buf))
|
||||||
|
n, err = conn.Read(another)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(len(another), n)
|
||||||
|
suite.Equal(buf, another)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseNetworkTestSuite) TestDialContextOk() {
|
||||||
|
conn, err := suite.net.DialContext(context.Background(), "tcp4", suite.EchoServerAddr())
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
buf := []byte{1, 2, 3, 4, 5}
|
||||||
|
n, err := conn.Write(buf)
|
||||||
|
suite.Equal(5, n)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
another := make([]byte, len(buf))
|
||||||
|
n, err = conn.Read(another)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(len(another), n)
|
||||||
|
suite.Equal(buf, another)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BaseNetworkTestSuite) TestDialContextClosed() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
_, err := suite.net.DialContext(ctx, "tcp4", suite.EchoServerAddr())
|
||||||
|
suite.ErrorIs(err, ctx.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkBase(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &BaseNetworkTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncruces/go-dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dnsCacheOptions = []dns.CacheOption{
|
||||||
|
dns.MaxCacheEntries(dns.DefaultMaxCacheEntries),
|
||||||
|
dns.MaxCacheTTL(time.Hour),
|
||||||
|
dns.NegativeCache(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDNS(u *url.URL) (*net.Resolver, error) {
|
||||||
|
if u == nil {
|
||||||
|
return dns.NewCachingResolver(nil, dnsCacheOptions...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "" {
|
||||||
|
u.Scheme = "udp"
|
||||||
|
}
|
||||||
|
if u.Scheme == "udp" && u.Host == "" {
|
||||||
|
u.Host = u.Path
|
||||||
|
u.Path = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "tls":
|
||||||
|
return dns.NewDoTResolver(u.Host, dns.DoTCache(dnsCacheOptions...))
|
||||||
|
case "https":
|
||||||
|
if u.Path == "" {
|
||||||
|
u.Path = "/dns-query"
|
||||||
|
}
|
||||||
|
|
||||||
|
return dns.NewDoHResolver(u.String(), dns.DoHCache(dnsCacheOptions...))
|
||||||
|
case "udp", "":
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported DNS %v", u)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := u.Port()
|
||||||
|
if port == "" {
|
||||||
|
port = "53"
|
||||||
|
}
|
||||||
|
|
||||||
|
hostport := net.JoinHostPort(u.Hostname(), port)
|
||||||
|
dialer := &net.Dialer{}
|
||||||
|
resolver := &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, "udp", hostport)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return dns.NewCachingResolver(resolver, dnsCacheOptions...), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package network_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/network/v2"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DNSTestSuite) TestDefault() {
|
||||||
|
resolver, err := network.GetDNS(nil)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.doTest(resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DNSTestSuite) TestDoH() {
|
||||||
|
for _, addr := range []string{"1.1.1.1", "cloudflare-dns.com"} {
|
||||||
|
suite.Run(addr, func() {
|
||||||
|
u, err := url.Parse("https://" + addr)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
resolver, err := network.GetDNS(u)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.doTest(resolver)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DNSTestSuite) TestDoT() {
|
||||||
|
u, err := url.Parse("tls://dns.google")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
resolver, err := network.GetDNS(u)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.doTest(resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DNSTestSuite) TestUDP() {
|
||||||
|
u, err := url.Parse("8.8.8.8")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
resolver, err := network.GetDNS(u)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.doTest(resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DNSTestSuite) doTest(resolver *net.Resolver) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ips, err := resolver.LookupIP(ctx, "ip4", "dns.google")
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Greater(len(ips), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDNS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &DNSTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package network_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EchoServer struct {
|
||||||
|
wg sync.WaitGroup
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
listener net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EchoServer) Run() {
|
||||||
|
e.wg.Go(func() {
|
||||||
|
<-e.ctx.Done()
|
||||||
|
e.listener.Close() //nolint: errcheck
|
||||||
|
})
|
||||||
|
|
||||||
|
e.wg.Go(func() {
|
||||||
|
for {
|
||||||
|
conn, err := e.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.wg.Go(func() {
|
||||||
|
<-e.ctx.Done()
|
||||||
|
conn.Close() //nolint: errcheck
|
||||||
|
})
|
||||||
|
e.wg.Go(func() {
|
||||||
|
e.process(conn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EchoServer) Stop() {
|
||||||
|
e.ctxCancel()
|
||||||
|
e.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EchoServer) Addr() string {
|
||||||
|
return e.listener.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EchoServer) process(conn io.ReadWriter) {
|
||||||
|
buf := [4096]byte{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := conn.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = conn.Write(buf[:n]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type EchoServerTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
echoServer *EchoServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EchoServerTestSuite) SetupSuite() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
suite.echoServer = &EchoServer{
|
||||||
|
ctx: ctx,
|
||||||
|
ctxCancel: cancel,
|
||||||
|
listener: listener,
|
||||||
|
}
|
||||||
|
suite.echoServer.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EchoServerTestSuite) TearDownSuite() {
|
||||||
|
suite.echoServer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EchoServerTestSuite) EchoServerAddr() string {
|
||||||
|
return suite.echoServer.Addr()
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type networkHTTPTransport struct {
|
||||||
|
userAgent string
|
||||||
|
next http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n networkHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
req.Header.Set("User-Agent", n.userAgent)
|
||||||
|
|
||||||
|
return n.next.RoundTrip(req) //nolint: wrapcheck
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Network contains a default implementation of the network.
|
||||||
|
//
|
||||||
|
// Please see [mtglib.Network] interface to get some basic idea behind this
|
||||||
|
// abstraction.
|
||||||
|
//
|
||||||
|
// This implementation is more simple that v1 because life shows that all
|
||||||
|
// this complexity, especially around circuit breakers and DoH is not really
|
||||||
|
// required. There is no chance that if DNS address is spoofed, that real
|
||||||
|
// IP would work as expected.
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultTimeout is a default timeout for establishing TCP connection.
|
||||||
|
DefaultTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// DefaultHTTPTimeout defines a default timeout for making HTTP request.
|
||||||
|
DefaultHTTPTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// DefaultIdleTimeout defines a timeout for idle HTTP connections
|
||||||
|
DefaultIdleTimeout = time.Minute
|
||||||
|
|
||||||
|
// DefaultTCPKeepAlivePeriod defines a time period between 2 consecuitive
|
||||||
|
// probes.
|
||||||
|
DefaultTCPKeepAlivePeriod = 10 * time.Second
|
||||||
|
|
||||||
|
// tcpLingerTimeout defines a number of seconds to wait for sending
|
||||||
|
// unacknowledged data.
|
||||||
|
tcpLingerTimeout = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrCannotDial = errors.New("cannot dial to any address")
|
||||||
|
|
||||||
|
type Network interface {
|
||||||
|
mtglib.Network
|
||||||
|
|
||||||
|
NativeDialer() *net.Dialer
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/essentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multiNetwork struct {
|
||||||
|
networks []Network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiNetwork) Dial(network, address string) (essentials.Conn, error) {
|
||||||
|
return m.DialContext(context.Background(), network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiNetwork) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
|
||||||
|
networks := m.networks
|
||||||
|
|
||||||
|
if len(networks) > 1 {
|
||||||
|
networks = make([]Network, len(m.networks))
|
||||||
|
copy(networks, m.networks)
|
||||||
|
|
||||||
|
rand.Shuffle(len(m.networks), func(i, j int) {
|
||||||
|
networks[i], networks[j] = networks[j], networks[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := make([]error, 1, len(networks)+1)
|
||||||
|
errs[0] = ErrCannotDial
|
||||||
|
|
||||||
|
for _, ntw := range networks {
|
||||||
|
conn, err := ntw.DialContext(ctx, network, address)
|
||||||
|
if err == nil {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiNetwork) NativeDialer() *net.Dialer {
|
||||||
|
return m.networks[0].NativeDialer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiNetwork) MakeHTTPClient(
|
||||||
|
dialFunc func(context.Context, string, string) (essentials.Conn, error),
|
||||||
|
) *http.Client {
|
||||||
|
if dialFunc == nil {
|
||||||
|
dialFunc = m.DialContext
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.networks[0].MakeHTTPClient(dialFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Join(networks ...Network) (Network, error) {
|
||||||
|
if len(networks) == 0 {
|
||||||
|
return nil, errors.New("cannot join no networks")
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiNetwork{
|
||||||
|
networks: networks,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/essentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
type network struct {
|
||||||
|
net.Dialer
|
||||||
|
|
||||||
|
httpTimeout time.Duration
|
||||||
|
idleTimeout time.Duration
|
||||||
|
userAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *network) Dial(network, address string) (essentials.Conn, error) {
|
||||||
|
return n.DialContext(context.Background(), network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *network) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
|
||||||
|
switch network {
|
||||||
|
case "tcp", "tcp4", "tcp6":
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported network %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := n.Dialer.DialContext(ctx, network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpConn := conn.(*net.TCPConn)
|
||||||
|
|
||||||
|
return tcpConn, setCommonSocketOptions(tcpConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *network) MakeHTTPClient(
|
||||||
|
dialFunc func(context.Context, string, string) (essentials.Conn, error),
|
||||||
|
) *http.Client {
|
||||||
|
if dialFunc == nil {
|
||||||
|
dialFunc = n.DialContext
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Timeout: n.httpTimeout,
|
||||||
|
Transport: networkHTTPTransport{
|
||||||
|
userAgent: n.userAgent,
|
||||||
|
next: &http.Transport{
|
||||||
|
IdleConnTimeout: n.idleTimeout,
|
||||||
|
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return dialFunc(ctx, network, address)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *network) NativeDialer() *net.Dialer {
|
||||||
|
return &n.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
dnsResolver *net.Resolver,
|
||||||
|
userAgent string,
|
||||||
|
tcpTimeout,
|
||||||
|
httpTimeout,
|
||||||
|
idleTimeout time.Duration,
|
||||||
|
) Network {
|
||||||
|
if dnsResolver == nil {
|
||||||
|
dnsResolver = net.DefaultResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
return &network{
|
||||||
|
Dialer: net.Dialer{
|
||||||
|
Timeout: tcpTimeout,
|
||||||
|
Resolver: dnsResolver,
|
||||||
|
FallbackDelay: -1,
|
||||||
|
},
|
||||||
|
userAgent: userAgent,
|
||||||
|
idleTimeout: idleTimeout,
|
||||||
|
httpTimeout: httpTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/essentials"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type proxyNetwork struct {
|
||||||
|
Network
|
||||||
|
client proxy.ContextDialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p proxyNetwork) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
|
||||||
|
conn, err := p.client.DialContext(ctx, network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return essentials.WrapNetConn(conn), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyNetwork(base Network, proxyURL *url.URL) (*proxyNetwork, error) {
|
||||||
|
socks, err := proxy.FromURL(proxyURL, base.NativeDialer())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot build proxy dialer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proxyNetwork{
|
||||||
|
Network: base,
|
||||||
|
client: socks.(proxy.ContextDialer),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setCommonSocketOptions(conn *net.TCPConn) error {
|
||||||
|
if err := conn.SetKeepAlivePeriod(DefaultTCPKeepAlivePeriod); err != nil {
|
||||||
|
return fmt.Errorf("cannot set time period of TCP keepalive probes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.SetLinger(tcpLingerTimeout); err != nil {
|
||||||
|
return fmt.Errorf("cannot set TCP linger timeout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConn, err := conn.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get underlying raw connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setSocketReuseAddrPort(rawConn); err != nil {
|
||||||
|
return fmt.Errorf("cannot setup SO_REUSEADDR/PORT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setSocketReuseAddrPort(conn syscall.RawConn) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
conn.Control(func(fd uintptr) { //nolint: errcheck
|
||||||
|
err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("cannot set SO_REUSEADDR: %w", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("cannot set SO_REUSEPORT: %w", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package network
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func setSocketReuseAddrPort(conn syscall.RawConn) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package network_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/network/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/things-go/go-socks5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocksProxyTestSuite struct {
|
||||||
|
EchoServerTestSuite
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
baseNetwork network.Network
|
||||||
|
|
||||||
|
noAuthURL *url.URL
|
||||||
|
authURL *url.URL
|
||||||
|
|
||||||
|
noAuthListener net.Listener
|
||||||
|
authListener net.Listener
|
||||||
|
|
||||||
|
noAuthServer *socks5.Server
|
||||||
|
authServer *socks5.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SocksProxyTestSuite) SetupSuite() {
|
||||||
|
suite.EchoServerTestSuite.SetupSuite()
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
suite.noAuthListener = listener
|
||||||
|
|
||||||
|
listener, err = net.Listen("tcp4", "127.0.0.1:0")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
suite.authListener = listener
|
||||||
|
|
||||||
|
suite.noAuthServer = socks5.NewServer()
|
||||||
|
suite.wg.Go(func() {
|
||||||
|
suite.noAuthServer.Serve(suite.noAuthListener) //nolint: errcheck
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.authServer = socks5.NewServer(
|
||||||
|
socks5.WithAuthMethods([]socks5.Authenticator{
|
||||||
|
socks5.UserPassAuthenticator{
|
||||||
|
Credentials: socks5.StaticCredentials{
|
||||||
|
"user": "pass",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
suite.wg.Go(func() {
|
||||||
|
suite.authServer.Serve(suite.authListener) //nolint: errcheck
|
||||||
|
})
|
||||||
|
|
||||||
|
parsed, err := url.Parse("socks5://" + suite.noAuthListener.Addr().String())
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
suite.noAuthURL = parsed
|
||||||
|
|
||||||
|
parsed, err = url.Parse("socks5://user:pass@" + suite.authListener.Addr().String())
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
suite.authURL = parsed
|
||||||
|
|
||||||
|
suite.baseNetwork = network.New(nil, "mtg", 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SocksProxyTestSuite) TestIncorrectSchema() {
|
||||||
|
parsed, err := url.Parse("http://hello")
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
_, err = network.NewProxyNetwork(suite.baseNetwork, parsed)
|
||||||
|
suite.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SocksProxyTestSuite) TestRead() {
|
||||||
|
testData := map[string][]*url.URL{
|
||||||
|
"noAuth": {suite.noAuthURL},
|
||||||
|
"auth": {suite.authURL},
|
||||||
|
"both": {suite.noAuthURL, suite.authURL},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, proxies := range testData {
|
||||||
|
suite.T().Run(name, func(t *testing.T) {
|
||||||
|
proxyNetworks := []network.Network{}
|
||||||
|
|
||||||
|
for _, u := range proxies {
|
||||||
|
value, err := network.NewProxyNetwork(suite.baseNetwork, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
proxyNetworks = append(proxyNetworks, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
netw, err := network.Join(proxyNetworks...)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
conn, err := netw.Dial("tcp4", suite.EchoServerAddr())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data := []byte{1, 2, 3}
|
||||||
|
n, err := conn.Write(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, len(data), n)
|
||||||
|
|
||||||
|
toRead := []byte{1, 2, 3, 4, 5}
|
||||||
|
n, err = conn.Read(toRead)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, len(data), n)
|
||||||
|
assert.Equal(t, data, toRead[:n])
|
||||||
|
assert.NotEqual(t, data, toRead)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SocksProxyTestSuite) TearDownSuite() {
|
||||||
|
suite.noAuthListener.Close() //nolint: errcheck
|
||||||
|
suite.authListener.Close() //nolint: errcheck
|
||||||
|
suite.wg.Wait()
|
||||||
|
suite.EchoServerTestSuite.TearDownSuite()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSocksProxy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &SocksProxyTestSuite{})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user