diff --git a/cli/proxy.go b/cli/proxy.go index eaf7035..50d608e 100644 --- a/cli/proxy.go +++ b/cli/proxy.go @@ -46,11 +46,11 @@ func (c *Proxy) Execute() error { // nolint: funlen TimeAttackDetector: timeattack.NewNoop(), EventStream: events.NewNoopStream(), - Secret: c.Config.Secret, - BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize), - CloakPort: c.Config.CloakPort.Value(mtglib.DefaultCloakPort), - IdleTimeout: c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout), - PreferIP: c.Config.PreferIP.Value(mtglib.DefaultPreferIP), + Secret: c.Config.Secret, + BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize), + DomainFrontingPort: c.Config.DomainFrontingPort.Value(mtglib.DefaultDomainFrontingPort), + IdleTimeout: c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout), + PreferIP: c.Config.PreferIP.Value(mtglib.DefaultPreferIP), } defer func() { diff --git a/config/config.go b/config/config.go index cbb2156..8414b9a 100644 --- a/config/config.go +++ b/config/config.go @@ -10,14 +10,14 @@ 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"` - CloakPort TypePort `json:"cloak-port"` - Concurrency uint `json:"concurrency"` - Defense 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"` @@ -85,14 +85,14 @@ 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"` - CloakPort uint `toml:"cloak-port" json:"cloak-port,omitempty"` - Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` - Defense 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"` diff --git a/example.config.toml b/example.config.toml index d573a16..618ec19 100644 --- a/example.config.toml +++ b/example.config.toml @@ -46,7 +46,7 @@ prefer-ip = "prefer-ipv6" # FakeTLS uses domain fronting protection. So it needs to know a port to # access. -cloak-port = 443 +domain-fronting-port = 443 # network defines different network-related settings [network] diff --git a/mtglib/conns.go b/mtglib/conns.go index e30cf0d..07b7086 100644 --- a/mtglib/conns.go +++ b/mtglib/conns.go @@ -1,8 +1,11 @@ package mtglib import ( + "bytes" "context" + "io" "net" + "sync" "time" ) @@ -43,3 +46,34 @@ func (c connTelegramTraffic) Write(b []byte) (int, error) { return n, err // nolint: wrapcheck } + +type connRewind struct { + net.Conn + + active io.Reader + buf bytes.Buffer + mutex sync.RWMutex +} + +func (c *connRewind) Read(p []byte) (int, error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + return c.active.Read(p) +} + +func (c *connRewind) Rewind() { + c.mutex.Lock() + defer c.mutex.Unlock() + + c.active = io.MultiReader(&c.buf, c.Conn) +} + +func newConnRewind(conn net.Conn) *connRewind { + rv := &connRewind{ + Conn: conn, + } + rv.active = io.TeeReader(conn, &rv.buf) + + return rv +} diff --git a/mtglib/init.go b/mtglib/init.go index 331bc38..44da825 100644 --- a/mtglib/init.go +++ b/mtglib/init.go @@ -17,17 +17,14 @@ var ( ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined") ErrEventStreamIsNotDefined = errors.New("event stream is not defined") ErrLoggerIsNotDefined = errors.New("logger is not defined") - - errCannotSendWelcomePacket = errors.New("cannot send welcome packet") - errReplayAttackDetected = errors.New("replay attack detected") ) const ( - DefaultConcurrency = 4096 - DefaultBufferSize = 16 * 1024 // 16 kib - DefaultCloakPort = 443 - DefaultIdleTimeout = time.Minute - DefaultPreferIP = "prefer-ipv6" + DefaultConcurrency = 4096 + DefaultBufferSize = 16 * 1024 // 16 kib + DefaultDomainFrontingPort = 443 + DefaultIdleTimeout = time.Minute + DefaultPreferIP = "prefer-ipv6" ) type Network interface { diff --git a/mtglib/proxy.go b/mtglib/proxy.go index b48d50f..4096438 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "strconv" "sync" "time" @@ -21,10 +22,11 @@ type Proxy struct { ctxCancel context.CancelFunc streamWaitGroup sync.WaitGroup - idleTimeout time.Duration - bufferSize int - workerPool *ants.PoolWithFunc - telegram *telegram.Telegram + idleTimeout time.Duration + bufferSize int + domainFrontAddress string + workerPool *ants.PoolWithFunc + telegram *telegram.Telegram secret Secret network Network @@ -59,9 +61,7 @@ func (p *Proxy) ServeConn(conn net.Conn) { ctx.logger.Info("Stream has been finished") }() - if err := p.doFakeTLSHandshake(ctx); err != nil { - p.logger.InfoError("faketls handshake is failed", err) - + if !p.doFakeTLSHandshake(ctx) { return } @@ -77,7 +77,8 @@ func (p *Proxy) ServeConn(conn net.Conn) { return } - rel := relay.AcquireRelay(ctx, p.logger.Named("relay"), p.bufferSize, p.idleTimeout) + rel := relay.AcquireRelay(ctx, + p.logger.Named("relay"), p.bufferSize, p.idleTimeout) defer relay.ReleaseRelay(rel) if err := rel.Process(ctx.clientConn, ctx.telegramConn); err != nil { @@ -122,38 +123,52 @@ func (p *Proxy) Shutdown() { p.workerPool.Release() } -func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) error { +func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool { rec := record.AcquireRecord() defer record.ReleaseRecord(rec) - if err := rec.Read(ctx.clientConn); err != nil { - return fmt.Errorf("cannot read client hello: %w", err) + rewind := newConnRewind(ctx.clientConn) + + if err := rec.Read(rewind); err != nil { + p.logger.InfoError("cannot read client hello", err) + p.doDomainFronting(ctx, rewind) + + return false } hello, err := faketls.ParseClientHello(p.secret.Key[:], rec.Payload.Bytes()) if err != nil { - return fmt.Errorf("cannot parse client hello: %w", err) + p.logger.InfoError("cannot parse client hello", err) + p.doDomainFronting(ctx, rewind) + + return false } if err := p.timeAttackDetector.Valid(hello.Time); err != nil { - return fmt.Errorf("invalid time: %w", err) + p.logger.InfoError("invalid faketls time", err) + p.doDomainFronting(ctx, rewind) + + return false } if p.antiReplayCache.SeenBefore(hello.SessionID) { - return errReplayAttackDetected + p.logger.Warning("replay attack has been detected!") + p.doDomainFronting(ctx, rewind) + + return false } - if err := faketls.SendWelcomePacket(ctx.clientConn, p.secret.Key[:], hello); err != nil { + if err := faketls.SendWelcomePacket(rewind, p.secret.Key[:], hello); err != nil { p.logger.InfoError("cannot send welcome packet", err) - return errCannotSendWelcomePacket + return false } ctx.clientConn = &faketls.Conn{ Conn: ctx.clientConn, } - return nil + return true } func (p *Proxy) doObfuscated2Handshake(ctx *streamContext) error { @@ -207,6 +222,25 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error { return nil } +func (p *Proxy) doDomainFronting(ctx context.Context, conn *connRewind) { + conn.Rewind() + + frontConn, err := p.network.DialContext(ctx, "tcp", p.domainFrontAddress) + if err != nil { + p.logger.WarningError("cannot dial to the fronting domain", err) + + return + } + + rel := relay.AcquireRelay(ctx, + p.logger.Named("domain-fronting"), p.bufferSize, p.idleTimeout) + defer relay.ReleaseRelay(rel) + + if err := rel.Process(conn, frontConn); err != nil { + p.logger.DebugError("domain fronting relay has been finished", err) + } +} + func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop, funlen switch { case opts.Network == nil: @@ -245,6 +279,11 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop, funlen bufferSize = DefaultBufferSize } + domainFrontingPort := int(opts.DomainFrontingPort) + if domainFrontingPort == 0 { + domainFrontingPort = DefaultDomainFrontingPort + } + ctx, cancel := context.WithCancel(context.Background()) proxy := &Proxy{ ctx: ctx, @@ -256,9 +295,11 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop, funlen ipBlocklist: opts.IPBlocklist, eventStream: opts.EventStream, logger: opts.Logger.Named("proxy"), - idleTimeout: idleTimeout, - bufferSize: int(bufferSize), - telegram: tg, + domainFrontAddress: net.JoinHostPort(opts.Secret.Host, + strconv.Itoa(domainFrontingPort)), + idleTimeout: idleTimeout, + bufferSize: int(bufferSize), + telegram: tg, } pool, err := ants.NewPoolWithFunc(int(concurrency), func(arg interface{}) { diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index 9067fb3..a20caaa 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -11,9 +11,9 @@ type ProxyOpts struct { EventStream EventStream Logger Logger - BufferSize uint - Concurrency uint - CloakPort uint - IdleTimeout time.Duration - PreferIP string + BufferSize uint + Concurrency uint + DomainFrontingPort uint + IdleTimeout time.Duration + PreferIP string }