From d1dd56550f32b81948f64ebba7d7d5e8581872d3 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Wed, 10 Mar 2021 14:41:21 +0300 Subject: [PATCH] Add correct configuration --- config.go | 383 +++++++++++++++++++++++++++++++++++++++++++- example.config.toml | 35 +--- go.mod | 1 + go.sum | 3 + mtglib/init.go | 4 +- mtglib/secret.go | 6 +- 6 files changed, 389 insertions(+), 43 deletions(-) diff --git a/config.go b/config.go index 51f43fe..5ef956d 100644 --- a/config.go +++ b/config.go @@ -5,14 +5,383 @@ import ( "encoding/json" "fmt" "io" + "net" + "net/url" + "regexp" + "strconv" + "strings" + "time" "github.com/9seconds/mtg/v2/mtglib" + "github.com/alecthomas/units" "github.com/pelletier/go-toml" ) +type configTypeHostPort struct { + host configTypeIP + port configTypePort +} + +func (c *configTypeHostPort) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + host, port, err := net.SplitHostPort(string(data)) + if err != nil { + return fmt.Errorf("incorrect host:port syntax: %w", err) + } + + if err := c.port.UnmarshalJSON([]byte(port)); err != nil { + return fmt.Errorf("incorrect port in host:port: %w", err) + } + + if err := c.host.UnmarshalText([]byte(host)); err != nil { + return fmt.Errorf("incorrect host: %w", err) + } + + return nil +} + +func (c configTypeHostPort) String() string { + return c.Value(net.IP{}, 0) +} + +func (c configTypeHostPort) Value(defaultHostValue net.IP, defaultPortValue uint) string { + return net.JoinHostPort(c.host.Value(defaultHostValue).String(), + strconv.Itoa(int(c.port.Value(defaultPortValue)))) +} + +type configTypePort struct { + value uint +} + +func (c *configTypePort) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + + intValue, err := strconv.ParseUint(string(data), 10, 16) + if err != nil { + return fmt.Errorf("port number is not a number: %w", err) + } + + if intValue == 0 || intValue > 65536 { + return fmt.Errorf("port number should be 0 < portNo < 65536: %d", intValue) + } + + c.value = uint(intValue) + + return nil +} + +func (c configTypePort) String() string { + return strconv.Itoa(int(c.value)) +} + +func (c configTypePort) Value(defaultValue uint) uint { + if c.value == 0 { + return defaultValue + } + + return c.value +} + +type configTypeBytes struct { + value uint +} + +func (c *configTypeBytes) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + value, err := units.ParseStrictBytes(strings.ToUpper(string(data))) + if err != nil { + return fmt.Errorf("incorrect bytes value: %w", err) + } + + if value < 0 { + return fmt.Errorf("%d should be positive number", value) + } + + c.value = uint(value) + + return nil +} + +func (c configTypeBytes) String() string { + return units.ToString(int64(c.value), 1024, "ib", "b") +} + +func (c configTypeBytes) Value(defaultValue uint) uint { + if c.value == 0 { + return defaultValue + } + + return c.value +} + +type configTypePreferIP struct { + value string +} + +func (c *configTypePreferIP) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + text := strings.ToLower(string(data)) + + switch text { + case "prefer-ipv4", "prefer-ipv6", "only-ipv4", "only-ipv6": + c.value = text + default: + return fmt.Errorf("incorrect prefer-ip value: %s", string(data)) + } + + return nil +} + +func (c *configTypePreferIP) String() string { + return c.value +} + +func (c *configTypePreferIP) Value(defaultValue string) string { + if c.value == "" { + return defaultValue + } + + return c.value +} + +type configTypeDuration struct { + value time.Duration +} + +func (c *configTypeDuration) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + dur, err := time.ParseDuration(strings.ToLower(string(data))) + if err != nil { + return fmt.Errorf("incorrect duration: %w", err) + } + + if dur < 0 { + return fmt.Errorf("%s should be positive duration", dur) + } + + c.value = dur + + return nil +} + +func (c configTypeDuration) String() string { + return c.value.String() +} + +func (c configTypeDuration) Value(defaultValue time.Duration) time.Duration { + if c.value == 0 { + return defaultValue + } + + return c.value +} + +type configTypeFloat struct { + value float64 +} + +func (c *configTypeFloat) UnmarshalJSON(data []byte) error { + value, err := strconv.ParseFloat(string(data), 64) + if err != nil { + return fmt.Errorf("incorrect float value: %w", err) + } + + if value < 0 { + return fmt.Errorf("%f should be positive", value) + } + + c.value = value + + return nil +} + +func (c configTypeFloat) String() string { + return strconv.FormatFloat(c.value, 'f', -1, 64) +} + +func (c configTypeFloat) Value(defaultValue float64) float64 { + if c.value < 0.00001 { + return defaultValue + } + + return c.value +} + +type configTypeIP struct { + value net.IP +} + +func (c *configTypeIP) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + ip := net.ParseIP(string(data)) + if ip == nil { + return fmt.Errorf("incorrect ip address: %s", string(data)) + } + + c.value = ip + + return nil +} + +func (c configTypeIP) String() string { + return c.value.String() +} + +func (c configTypeIP) Value(defaultValue net.IP) net.IP { + if c.value == nil { + return defaultValue + } + + return c.value +} + +type configTypeURL struct { + value *url.URL +} + +func (c *configTypeURL) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + value, err := url.Parse(string(data)) + if err != nil { + return fmt.Errorf("incorrect URL: %w", err) + } + + c.value = value + + return nil +} + +func (c configTypeURL) String() string { + if c.value == nil { + return "" + } + + return c.value.String() +} + +func (c configTypeURL) Value(defaultValue *url.URL) *url.URL { + if c.value == nil { + return defaultValue + } + + return c.value +} + +type configTypeMetricPrefix struct { + value string +} + +func (c *configTypeMetricPrefix) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + prefix := string(data) + + if ok, err := regexp.MatchString("^[a-z0-9]+$", prefix); !ok || err != nil { + return fmt.Errorf("incorrect metric prefix: %s", prefix) + } + + c.value = prefix + + return nil +} + +func (c configTypeMetricPrefix) String() string { + return c.value +} + +func (c configTypeMetricPrefix) Value(defaultValue string) string { + if c.value == "" { + return defaultValue + } + + return c.value +} + +type configTypeHTTPPath struct { + value string +} + +func (c *configTypeHTTPPath) UnmarshalText(data []byte) error { // nolint: unparam + if len(data) > 0 { + c.value = "/" + strings.Trim(string(data), "/") + } + + return nil +} + +func (c configTypeHTTPPath) String() string { + return c.value +} + +func (c configTypeHTTPPath) Value(defaultValue string) string { + if c.value == "" { + return defaultValue + } + + return c.value +} + type config struct { - Debug bool `json:"debug"` - Secret mtglib.Secret `json:"secret"` + Debug bool `json:"debug"` + Secret mtglib.Secret `json:"secret"` + BindTo configTypeHostPort `json:"bind-to"` + TCPBuffer configTypeBytes `json:"tcp-buffer"` + PreferIP configTypePreferIP `json:"prefer-ip"` + CloakPort configTypePort `json:"cloak-port"` + Probes struct { + Time struct { + Enabled bool `json:"enabled"` + AllowSkewness configTypeDuration `json:"allow-skewness"` + } `json:"time"` + AntiReplay struct { + Enabled bool `json:"enabled"` + MaxSize configTypeBytes `json:"max-size"` + ErrorRate configTypeFloat `json:"error-rate"` + } `json:"anti-replay"` + } `json:"probes"` + Network struct { + PublicIP struct { + IPv4 configTypeIP `json:"ipv4"` + IPv6 configTypeIP `json:"ipv6"` + } `json:"public-ip"` + DOHIP configTypeIP `json:"doh-ip"` + Proxies []configTypeURL `json:"proxies"` + } `json:"network"` + Stats struct { + StatsD struct { + Enabled bool `json:"enabled"` + Address configTypeHostPort `json:"address"` + MetricPrefix configTypeMetricPrefix `json:"metric-prefix"` + } `json:"statsd"` + Prometheus struct { + Enabled bool `json:"enabled"` + BindTo configTypeHostPort `json:"bind-to"` + HTTPPath configTypeHTTPPath `json:"http-path"` + MetricPrefix configTypeMetricPrefix `json:"metric-prefix"` + } `json:"prometheus"` + } `json:"stats"` } func (c *config) Validate() error { @@ -36,9 +405,9 @@ type configRaw struct { AllowSkewness string `toml:"allow-skewness" json:"allow-skewness"` } `toml:"time" json:"time"` AntiReplay struct { - Enabled bool `toml:"enabled" json:"enabled"` - MaxSize string `toml:"max-size" json:"max-size"` - TTL string `toml:"ttl" json:"ttl"` + Enabled bool `toml:"enabled" json:"enabled"` + MaxSize string `toml:"max-size" json:"max-size"` + ErrorRate float64 `toml:"error-rate" json:"error-rate"` } `toml:"anti-replay" json:"anti-replay"` } `toml:"probes" json:"probes"` Network struct { @@ -46,8 +415,8 @@ type configRaw struct { IPv4 string `toml:"ipv4" json:"ipv4"` IPv6 string `toml:"ipv6" json:"ipv6"` } `toml:"public-ip" json:"public-ip"` - DOHHostname string `toml:"doh-hostname" json:"doh-hostname"` - Proxies []string `toml:"proxies" json:"proxies"` + DOHIP string `toml:"doh-ip" json:"doh-ip"` + Proxies []string `toml:"proxies" json:"proxies"` } `toml:"network" json:"network"` Stats struct { StatsD struct { diff --git a/example.config.toml b/example.config.toml index 3397220..ee896d0 100644 --- a/example.config.toml +++ b/example.config.toml @@ -38,7 +38,7 @@ tcp-buffer = "4kb" # Only ipv6 connectivity is used # - only-ipv4: # Only ipv4 connectivity is used -prefer-ips = "prefer-ipv6" +prefer-ip = "prefer-ipv6" # FakeTLS uses domain fronting protection. So it needs to know a port to # access. @@ -58,7 +58,7 @@ cloak-port = 443 # it has to access. # # By default we use Quad9. -doh-hostname = "9.9.9.9" +doh-ip = "9.9.9.9" # mtg can work via proxies (for now, we support only socks5). Proxy # configuration is done via list. So, you can specify many proxies @@ -75,7 +75,6 @@ doh-hostname = "9.9.9.9" # # socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s # - # Only socks5 proxy is used. user/password is optional. As you can # see, you can specify some parameters in GET query. These parameters # configure circuit breaker. @@ -102,31 +101,6 @@ proxies = [ ipv4 = "" ipv6 = "" -# you can redefine a dialer for mtg. Dialer is how we 'dial' to either -# some external services or telegram. empty string means default -# connectivity. -# -# it is also possible to use socks5 or shadowsocks here -# -# socks5 example: -# socks5://user:password@host:port -# shadowsocks example (SIP002): -# ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888 -# -# You can define 2 dialers here: telegram and default. Telegram dialer -# is used to connect to Telegram servers only. Default is used for other -# purposes, like accessing ifconfig.co to obtains public address (DNS is -# resolved via DoH) -# -# Please also be aware that dialers are only doing TCP. If UDP is -# required (for statsd for example), then these dialers are going to be -# ignored. -# -# If telegram dialer is not defined, a default one is going to be used. -[network.dialers] -telegram = "" -default = "" - # FakeTLS can compare timestamps to prevent probes. Each message has # encrypted timestamp. So, mtg can compare this timestamp and decide if # we need to proceed with connection or not. @@ -154,8 +128,9 @@ enabled = true # that we can go over this limit for 10-20% under some conditions and # architectures. max-size = "16mb" -# TTL for each cache record. -ttl = "8h" +# we use stable bloom filters for anti-replay cache. This helps +# to maintain a desired error ratio. +error-rate = 0.0001 # statsd statistics integration. [stats.statsd] diff --git a/go.mod b/go.mod index f201e45..910aef8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/9seconds/mtg/v2 go 1.16 require ( + github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 github.com/kr/pretty v0.1.0 // indirect diff --git a/go.sum b/go.sum index 817b6fb..497262e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= @@ -24,6 +26,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/mtglib/init.go b/mtglib/init.go index 1fa135a..e5e1a0d 100644 --- a/mtglib/init.go +++ b/mtglib/init.go @@ -2,6 +2,4 @@ package mtglib import "errors" -var ( - ErrSecretEmpty = errors.New("secret is empty") -) +var ErrSecretEmpty = errors.New("secret is empty") diff --git a/mtglib/secret.go b/mtglib/secret.go index 8fe6a90..4a4cd80 100644 --- a/mtglib/secret.go +++ b/mtglib/secret.go @@ -58,10 +58,10 @@ func (s *Secret) UnmarshalText(data []byte) error { } func (s Secret) Base64() string { - data := append([]byte{238}, s.Key...) // 238 = hex ee - data = append(data, s.Host...) + data := append([]byte{238}, s.Key...) // 238 = hex ee + data = append(data, s.Host...) - return base64.RawURLEncoding.EncodeToString(data) + return base64.RawURLEncoding.EncodeToString(data) } func (s Secret) String() string {