diff --git a/example.config.toml b/example.config.toml index fe3c883..c96e354 100644 --- a/example.config.toml +++ b/example.config.toml @@ -48,6 +48,14 @@ prefer-ip = "prefer-ipv6" # access. domain-fronting-port = 443 +# 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. +# +# Sometimes time can be skewed so we accept all messages within a +# time range of this parameter. +tolerate-time-skewness = "5s" + # network defines different network-related settings [network] # please be aware that mtg needs to do some external requests. For @@ -111,19 +119,6 @@ tcp = "5s" http = "10s" idle = "1m" -# 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. -# -# Please ensure that you have some ntp active on this host. Otherwise, -# you can endup with badly performing proxy. -[defense.time] -# You can enable/disable that. A good idea is always enable. -enabled = true -# Time can be skewed by many reasons. So, this is a time interval -# when message is cosidered as a good one. -allow-skewness = "5s" - # Some countries do active probing on Telegram connections. This technique # allows to protect from such effort. # diff --git a/internal/cli/proxy.go b/internal/cli/proxy.go index 713afcb..b20b39d 100644 --- a/internal/cli/proxy.go +++ b/internal/cli/proxy.go @@ -12,7 +12,6 @@ import ( "github.com/9seconds/mtg/v2/logger" "github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/stats" - "github.com/9seconds/mtg/v2/timeattack" "github.com/rs/zerolog" ) @@ -41,12 +40,11 @@ func (c *Proxy) Execute() error { ctx := utils.RootContext() opts := mtglib.ProxyOpts{ - Logger: logger.NewZeroLogger(zerolog.New(os.Stdout).With().Timestamp().Logger()), - Network: c.Network, - AntiReplayCache: antireplay.NewNoop(), - IPBlocklist: ipblocklist.NewNoop(), - TimeAttackDetector: timeattack.NewNoop(), - EventStream: events.NewNoopStream(), + Logger: logger.NewZeroLogger(zerolog.New(os.Stdout).With().Timestamp().Logger()), + Network: c.Network, + AntiReplayCache: antireplay.NewNoop(), + IPBlocklist: ipblocklist.NewNoop(), + EventStream: events.NewNoopStream(), Secret: c.Config.Secret, BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize), @@ -58,7 +56,6 @@ func (c *Proxy) Execute() error { opts.Logger.BindStr("configuration", c.Config.String()).Debug("configuration") c.setupAntiReplayCache(&opts) - c.setupTimeAttackDetector(&opts) if err := c.setupIPBlocklist(&opts); err != nil { return fmt.Errorf("cannot setup ipblocklist: %w", err) @@ -98,16 +95,6 @@ func (c *Proxy) setupAntiReplayCache(opts *mtglib.ProxyOpts) { ) } -func (c *Proxy) setupTimeAttackDetector(opts *mtglib.ProxyOpts) { - if !c.Config.Defense.Time.Enabled { - return - } - - opts.TimeAttackDetector = timeattack.NewDetector( - c.Config.Defense.Time.AllowSkewness.Value(timeattack.DefaultDuration), - ) -} - func (c *Proxy) setupIPBlocklist(opts *mtglib.ProxyOpts) error { if !c.Config.Defense.Blocklist.Enabled { return nil diff --git a/internal/config/config.go b/internal/config/config.go index 8414b9a..a634c7b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,18 +10,15 @@ import ( ) type Config struct { - Debug bool `json:"debug"` - Secret mtglib.Secret `json:"secret"` - BindTo TypeHostPort `json:"bind-to"` - TCPBuffer TypeBytes `json:"tcp-buffer"` - PreferIP TypePreferIP `json:"prefer-ip"` - DomainFrontingPort TypePort `json:"domain-fronting-port"` - Concurrency uint `json:"concurrency"` - Defense struct { - Time struct { - Enabled bool `json:"enabled"` - AllowSkewness TypeDuration `json:"allow-skewness"` - } `json:"time"` + Debug bool `json:"debug"` + Secret mtglib.Secret `json:"secret"` + BindTo TypeHostPort `json:"bind-to"` + TCPBuffer TypeBytes `json:"tcp-buffer"` + PreferIP TypePreferIP `json:"prefer-ip"` + DomainFrontingPort TypePort `json:"domain-fronting-port"` + TolerateTimeSkewness TypeDuration `json:"tolerate-time-skewness"` + Concurrency uint `json:"concurrency"` + Defense struct { AntiReplay struct { Enabled bool `json:"enabled"` MaxSize TypeBytes `json:"max-size"` @@ -85,18 +82,15 @@ func (c *Config) String() string { } type configRaw struct { - Debug bool `toml:"debug" json:"debug,omitempty"` - Secret string `toml:"secret" json:"secret"` - BindTo string `toml:"bind-to" json:"bind-to"` - TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer,omitempty"` - PreferIP string `toml:"prefer-ip" json:"prefer-ip,omitempty"` - DomainFrontingPort uint `toml:"domain-fronting-port" json:"domain-fronting-port,omitempty"` - Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` - Defense struct { - Time struct { - Enabled bool `toml:"enabled" json:"enabled,omitempty"` - AllowSkewness string `toml:"allow-skewness" json:"allow-skewness,omitempty"` - } `toml:"time" json:"time,omitempty"` + Debug bool `toml:"debug" json:"debug,omitempty"` + Secret string `toml:"secret" json:"secret"` + BindTo string `toml:"bind-to" json:"bind-to"` + TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer,omitempty"` + PreferIP string `toml:"prefer-ip" json:"prefer-ip,omitempty"` + DomainFrontingPort uint `toml:"domain-fronting-port" json:"domain-fronting-port,omitempty"` + TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerate-time-skewness,omitempty"` + Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` + Defense struct { AntiReplay struct { Enabled bool `toml:"enabled" json:"enabled,omitempty"` MaxSize string `toml:"max-size" json:"max-size,omitempty"` diff --git a/mtglib/init.go b/mtglib/init.go index e6807d7..d4f265b 100644 --- a/mtglib/init.go +++ b/mtglib/init.go @@ -42,10 +42,6 @@ var ( // create a proxy but anti replay cache value is undefined. ErrAntiReplayCacheIsNotDefined = errors.New("anti-replay cache is not defined") - // ErrTimeAttackDetectorIsNotDefined is returned if you are trying to - // create a proxy but time attack detector is not defined. - ErrTimeAttackDetectorIsNotDefined = errors.New("time attack detector is not defined") - // ErrIPBlocklistIsNotDefined is returned if you are trying to // create a proxy but ip blocklist instance is not defined. ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined") @@ -75,6 +71,10 @@ const ( // in case of idling. DefaultIdleTimeout = time.Minute + // DefaultTolerateTimeSkewness is a default timeout for time + // skewness on a faketls timeout verification. + DefaultTolerateTimeSkewness = 3 * time.Second + // DefaultPreferIP is a default value for Telegram IP connectivity // preference. DefaultPreferIP = "prefer-ipv6" @@ -206,18 +206,6 @@ type EventStream interface { Send(context.Context, Event) } -// TimeAttackDetector is an abstraction that checks a time, taken from -// the faketls client hello message. This timestamp is encoded into -// client-generated random bytes and can be extracted after some client -// hello verification. -// -// This is mostly to prevent replay attacks. -type TimeAttackDetector interface { - // Valid returns an error if timestamp is invalid or should not be - // accepted. - Valid(time.Time) error -} - // Logger defines an interface of the logger used by mtglib. // // Each logger has a name. It is possible to stack names to organize diff --git a/mtglib/internal/faketls/client_hello.go b/mtglib/internal/faketls/client_hello.go index 51e1fe5..2062a8a 100644 --- a/mtglib/internal/faketls/client_hello.go +++ b/mtglib/internal/faketls/client_hello.go @@ -19,6 +19,26 @@ type ClientHello struct { CipherSuite uint16 } +func (c ClientHello) Valid(hostname string, tolerateTimeSkewness time.Duration) error { + if c.Host != "" && c.Host != hostname { + return fmt.Errorf("incorrect hostname %s", hostname) + } + + now := time.Now() + + timeDiff := now.Sub(c.Time) + if timeDiff < 0 { + timeDiff = -timeDiff + } + + if timeDiff > tolerateTimeSkewness { + return fmt.Errorf("incorrect timestamp. got=%d, now=%d, diff=%s", + c.Time.Unix(), now.Unix(), timeDiff.String()) + } + + return nil +} + func ParseClientHello(secret, handshake []byte) (ClientHello, error) { hello := ClientHello{} diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 8a550fb..e07a9fe 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -23,19 +23,19 @@ type Proxy struct { ctxCancel context.CancelFunc streamWaitGroup sync.WaitGroup - idleTimeout time.Duration - bufferSize int - domainFrontingPort int - workerPool *ants.PoolWithFunc - telegram *telegram.Telegram + idleTimeout time.Duration + tolerateTimeSkewness time.Duration + bufferSize int + domainFrontingPort int + workerPool *ants.PoolWithFunc + telegram *telegram.Telegram - secret Secret - network Network - antiReplayCache AntiReplayCache - timeAttackDetector TimeAttackDetector - ipBlocklist IPBlocklist - eventStream EventStream - logger Logger + secret Secret + network Network + antiReplayCache AntiReplayCache + ipBlocklist IPBlocklist + eventStream EventStream + logger Logger } // DomainFrontingAddress returns a host:port pair for a fronting domain. @@ -159,15 +159,11 @@ func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool { return false } - if hello.Host != "" && hello.Host != p.secret.Host { - p.logger.BindStr("hostname", hello.Host).Info("incorrect domain was found in SNI") - p.doDomainFronting(ctx, rewind) - - return false - } - - if err := p.timeAttackDetector.Valid(hello.Time); err != nil { - p.logger.InfoError("invalid faketls time", err) + if err := hello.Valid(p.secret.Host, p.tolerateTimeSkewness); err != nil { + p.logger. + BindStr("hostname", hello.Host). + BindStr("hello-time", hello.Time.String()). + InfoError("invalid faketls client hello", err) p.doDomainFronting(ctx, rewind) return false @@ -281,19 +277,19 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { ctx, cancel := context.WithCancel(context.Background()) proxy := &Proxy{ - ctx: ctx, - ctxCancel: cancel, - secret: opts.Secret, - network: opts.Network, - antiReplayCache: opts.AntiReplayCache, - timeAttackDetector: opts.TimeAttackDetector, - ipBlocklist: opts.IPBlocklist, - eventStream: opts.EventStream, - logger: opts.getLogger("proxy"), - domainFrontingPort: opts.getDomainFrontingPort(), - idleTimeout: opts.getIdleTimeout(), - bufferSize: opts.getBufferSize(), - telegram: tg, + ctx: ctx, + ctxCancel: cancel, + secret: opts.Secret, + network: opts.Network, + antiReplayCache: opts.AntiReplayCache, + ipBlocklist: opts.IPBlocklist, + eventStream: opts.EventStream, + logger: opts.getLogger("proxy"), + domainFrontingPort: opts.getDomainFrontingPort(), + tolerateTimeSkewness: opts.getTolerateTimeSkewness(), + idleTimeout: opts.getIdleTimeout(), + bufferSize: opts.getBufferSize(), + telegram: tg, } pool, err := ants.NewPoolWithFunc(opts.getConcurrency(), diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index 8da37f8..c2fdc69 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -23,11 +23,6 @@ type ProxyOpts struct { // This is a mandatory setting. AntiReplayCache AntiReplayCache - // TimeAttackDetector defines an instance of timeattack detector. - // - // This is a mandatory setting. - TimeAttackDetector TimeAttackDetector - // IPBlocklist defines an instance of IP blocklist. // // This is a mandatory setting. @@ -80,6 +75,15 @@ type ProxyOpts struct { // This is an optional setting. IdleTimeout time.Duration + // TolerateTimeSkewness is a time boundary that defines a time + // range where faketls timestamp is acceptable. + // + // This means that if if you got a timestamp X, now is Y, then + // if |X-Y| < TolerateTimeSkewness, then you accept a packet. + // + // This is an optional setting. + TolerateTimeSkewness time.Duration + // PreferIP defines an IP connectivity preference. Valid values are: // 'prefer-ipv4', 'prefer-ipv6', 'only-ipv4', 'only-ipv6'. // @@ -97,8 +101,6 @@ func (p ProxyOpts) valid() error { return ErrIPBlocklistIsNotDefined case p.EventStream == nil: return ErrEventStreamIsNotDefined - case p.TimeAttackDetector == nil: - return ErrTimeAttackDetectorIsNotDefined case p.Logger == nil: return ErrLoggerIsNotDefined case !p.Secret.Valid(): @@ -140,6 +142,14 @@ func (p ProxyOpts) getIdleTimeout() time.Duration { return p.IdleTimeout } +func (p ProxyOpts) getTolerateTimeSkewness() time.Duration { + if p.TolerateTimeSkewness == 0 { + return DefaultTolerateTimeSkewness + } + + return p.TolerateTimeSkewness +} + func (p ProxyOpts) getPreferIP() string { if p.PreferIP == "" { return DefaultPreferIP diff --git a/mtglib/proxy_test.go b/mtglib/proxy_test.go index 6e767ee..3d3171b 100644 --- a/mtglib/proxy_test.go +++ b/mtglib/proxy_test.go @@ -16,7 +16,6 @@ import ( "github.com/9seconds/mtg/v2/logger" "github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/network" - "github.com/9seconds/mtg/v2/timeattack" "github.com/stretchr/testify/suite" ) @@ -46,13 +45,12 @@ func (suite *ProxyTestSuite) SetupSuite() { suite.NoError(err) suite.opts = &mtglib.ProxyOpts{ - Secret: mtglib.GenerateSecret("httpbin.org"), - Network: ntw, - AntiReplayCache: antireplay.NewNoop(), - TimeAttackDetector: timeattack.NewNoop(), - IPBlocklist: ipblocklist.NewNoop(), - EventStream: events.NewNoopStream(), - Logger: logger.NewNoopLogger(), + Secret: mtglib.GenerateSecret("httpbin.org"), + Network: ntw, + AntiReplayCache: antireplay.NewNoop(), + IPBlocklist: ipblocklist.NewNoop(), + EventStream: events.NewNoopStream(), + Logger: logger.NewNoopLogger(), } proxy, err := mtglib.NewProxy(*suite.opts) @@ -118,14 +116,6 @@ func (suite *ProxyTestSuite) TestCannotInitNoEventStream() { suite.Error(err) } -func (suite *ProxyTestSuite) TestCannotInitNoTimeAttackDetector() { - opts := *suite.opts - opts.TimeAttackDetector = nil - - _, err := mtglib.NewProxy(opts) - suite.Error(err) -} - func (suite *ProxyTestSuite) TestCannotInitNoLogger() { opts := *suite.opts opts.Logger = nil diff --git a/timeattack/detector.go b/timeattack/detector.go deleted file mode 100644 index 4ed1179..0000000 --- a/timeattack/detector.go +++ /dev/null @@ -1,39 +0,0 @@ -package timeattack - -import ( - "fmt" - "time" - - "github.com/9seconds/mtg/v2/mtglib" -) - -type detector struct { - time.Duration -} - -func (d detector) Valid(then time.Time) error { - now := time.Now() - - diff := now.Sub(then) - if diff < 0 { - diff = -diff - } - - if diff > d.Duration { - return fmt.Errorf("time is invalid. now=%d, then=%d, diff=%v", - now.Unix(), - then.Unix(), - diff) - } - - return nil -} - -// NewDetector returns a new TimeAttackDetector which validates that -// timestamp belongs to intervar [X-duration, X+duration], so a small -// timeshift is acceptable. -func NewDetector(duration time.Duration) mtglib.TimeAttackDetector { - return detector{ - Duration: duration, - } -} diff --git a/timeattack/detector_test.go b/timeattack/detector_test.go deleted file mode 100644 index dd4bd9f..0000000 --- a/timeattack/detector_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package timeattack_test - -import ( - "testing" - "time" - - "github.com/9seconds/mtg/v2/timeattack" - "github.com/stretchr/testify/suite" -) - -type DetectorTestSuite struct { - suite.Suite -} - -func (suite *DetectorTestSuite) TestOp() { - d := timeattack.NewDetector(time.Second) - - suite.NoError(d.Valid(time.Now())) - suite.NoError(d.Valid(time.Now().Add(100 * time.Millisecond))) - suite.NoError(d.Valid(time.Now().Add(-100 * time.Millisecond))) - suite.Error(d.Valid(time.Now().Add(time.Hour))) - suite.Error(d.Valid(time.Now().Add(-time.Hour))) -} - -func TestDetector(t *testing.T) { - t.Parallel() - suite.Run(t, &DetectorTestSuite{}) -} diff --git a/timeattack/init.go b/timeattack/init.go deleted file mode 100644 index e2f9788..0000000 --- a/timeattack/init.go +++ /dev/null @@ -1,10 +0,0 @@ -// TimeAttack has implementation of mtglib.TimeAttackDetector. -package timeattack - -import "time" - -// DefaultDuration is a default duration when timestamps are acceptable. -// -// It means that all timestamps which are X-DefaultDuration <= X <= -// X+DefaultDuration are fine. -const DefaultDuration = 5 * time.Second diff --git a/timeattack/noop.go b/timeattack/noop.go deleted file mode 100644 index e3827b8..0000000 --- a/timeattack/noop.go +++ /dev/null @@ -1,16 +0,0 @@ -package timeattack - -import ( - "time" - - "github.com/9seconds/mtg/v2/mtglib" -) - -type noop struct{} - -func (n noop) Valid(_ time.Time) error { return nil } - -// NewNoop returns TimeAttackDetector which accepts all timestamps. -func NewNoop() mtglib.TimeAttackDetector { - return noop{} -} diff --git a/timeattack/noop_test.go b/timeattack/noop_test.go deleted file mode 100644 index 4b3da79..0000000 --- a/timeattack/noop_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package timeattack_test - -import ( - "testing" - "time" - - "github.com/9seconds/mtg/v2/timeattack" - "github.com/stretchr/testify/suite" -) - -type NoopTestSuite struct { - suite.Suite -} - -func (suite *NoopTestSuite) TestOp() { - d := timeattack.NewNoop() - - suite.NoError(d.Valid(time.Now())) - suite.NoError(d.Valid(time.Now().Add(time.Hour))) - suite.NoError(d.Valid(time.Now().Add(-time.Hour))) -} - -func TestNoop(t *testing.T) { - t.Parallel() - suite.Run(t, &NoopTestSuite{}) -}