Implement domain fronting

This commit is contained in:
9seconds
2021-03-26 14:45:00 +03:00
parent 9b78f766e8
commit bddf180575
7 changed files with 127 additions and 55 deletions
+5 -5
View File
@@ -46,11 +46,11 @@ func (c *Proxy) Execute() error { // nolint: funlen
TimeAttackDetector: timeattack.NewNoop(), TimeAttackDetector: timeattack.NewNoop(),
EventStream: events.NewNoopStream(), EventStream: events.NewNoopStream(),
Secret: c.Config.Secret, Secret: c.Config.Secret,
BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize), BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize),
CloakPort: c.Config.CloakPort.Value(mtglib.DefaultCloakPort), DomainFrontingPort: c.Config.DomainFrontingPort.Value(mtglib.DefaultDomainFrontingPort),
IdleTimeout: c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout), IdleTimeout: c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout),
PreferIP: c.Config.PreferIP.Value(mtglib.DefaultPreferIP), PreferIP: c.Config.PreferIP.Value(mtglib.DefaultPreferIP),
} }
defer func() { defer func() {
+16 -16
View File
@@ -10,14 +10,14 @@ import (
) )
type Config struct { type Config struct {
Debug bool `json:"debug"` Debug bool `json:"debug"`
Secret mtglib.Secret `json:"secret"` Secret mtglib.Secret `json:"secret"`
BindTo TypeHostPort `json:"bind-to"` BindTo TypeHostPort `json:"bind-to"`
TCPBuffer TypeBytes `json:"tcp-buffer"` TCPBuffer TypeBytes `json:"tcp-buffer"`
PreferIP TypePreferIP `json:"prefer-ip"` PreferIP TypePreferIP `json:"prefer-ip"`
CloakPort TypePort `json:"cloak-port"` DomainFrontingPort TypePort `json:"domain-fronting-port"`
Concurrency uint `json:"concurrency"` Concurrency uint `json:"concurrency"`
Defense struct { Defense struct {
Time struct { Time struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
AllowSkewness TypeDuration `json:"allow-skewness"` AllowSkewness TypeDuration `json:"allow-skewness"`
@@ -85,14 +85,14 @@ func (c *Config) String() string {
} }
type configRaw struct { type configRaw struct {
Debug bool `toml:"debug" json:"debug,omitempty"` Debug bool `toml:"debug" json:"debug,omitempty"`
Secret string `toml:"secret" json:"secret"` Secret string `toml:"secret" json:"secret"`
BindTo string `toml:"bind-to" json:"bind-to"` BindTo string `toml:"bind-to" json:"bind-to"`
TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer,omitempty"` TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer,omitempty"`
PreferIP string `toml:"prefer-ip" json:"prefer-ip,omitempty"` PreferIP string `toml:"prefer-ip" json:"prefer-ip,omitempty"`
CloakPort uint `toml:"cloak-port" json:"cloak-port,omitempty"` DomainFrontingPort uint `toml:"domain-fronting-port" json:"domain-fronting-port,omitempty"`
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
Defense struct { Defense struct {
Time struct { Time struct {
Enabled bool `toml:"enabled" json:"enabled,omitempty"` Enabled bool `toml:"enabled" json:"enabled,omitempty"`
AllowSkewness string `toml:"allow-skewness" json:"allow-skewness,omitempty"` AllowSkewness string `toml:"allow-skewness" json:"allow-skewness,omitempty"`
+1 -1
View File
@@ -46,7 +46,7 @@ prefer-ip = "prefer-ipv6"
# FakeTLS uses domain fronting protection. So it needs to know a port to # FakeTLS uses domain fronting protection. So it needs to know a port to
# access. # access.
cloak-port = 443 domain-fronting-port = 443
# network defines different network-related settings # network defines different network-related settings
[network] [network]
+34
View File
@@ -1,8 +1,11 @@
package mtglib package mtglib
import ( import (
"bytes"
"context" "context"
"io"
"net" "net"
"sync"
"time" "time"
) )
@@ -43,3 +46,34 @@ func (c connTelegramTraffic) Write(b []byte) (int, error) {
return n, err // nolint: wrapcheck 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
}
+5 -8
View File
@@ -17,17 +17,14 @@ var (
ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined") ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined")
ErrEventStreamIsNotDefined = errors.New("event stream is not defined") ErrEventStreamIsNotDefined = errors.New("event stream is not defined")
ErrLoggerIsNotDefined = errors.New("logger is not defined") ErrLoggerIsNotDefined = errors.New("logger is not defined")
errCannotSendWelcomePacket = errors.New("cannot send welcome packet")
errReplayAttackDetected = errors.New("replay attack detected")
) )
const ( const (
DefaultConcurrency = 4096 DefaultConcurrency = 4096
DefaultBufferSize = 16 * 1024 // 16 kib DefaultBufferSize = 16 * 1024 // 16 kib
DefaultCloakPort = 443 DefaultDomainFrontingPort = 443
DefaultIdleTimeout = time.Minute DefaultIdleTimeout = time.Minute
DefaultPreferIP = "prefer-ipv6" DefaultPreferIP = "prefer-ipv6"
) )
type Network interface { type Network interface {
+61 -20
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"strconv"
"sync" "sync"
"time" "time"
@@ -21,10 +22,11 @@ type Proxy struct {
ctxCancel context.CancelFunc ctxCancel context.CancelFunc
streamWaitGroup sync.WaitGroup streamWaitGroup sync.WaitGroup
idleTimeout time.Duration idleTimeout time.Duration
bufferSize int bufferSize int
workerPool *ants.PoolWithFunc domainFrontAddress string
telegram *telegram.Telegram workerPool *ants.PoolWithFunc
telegram *telegram.Telegram
secret Secret secret Secret
network Network network Network
@@ -59,9 +61,7 @@ func (p *Proxy) ServeConn(conn net.Conn) {
ctx.logger.Info("Stream has been finished") ctx.logger.Info("Stream has been finished")
}() }()
if err := p.doFakeTLSHandshake(ctx); err != nil { if !p.doFakeTLSHandshake(ctx) {
p.logger.InfoError("faketls handshake is failed", err)
return return
} }
@@ -77,7 +77,8 @@ func (p *Proxy) ServeConn(conn net.Conn) {
return 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) defer relay.ReleaseRelay(rel)
if err := rel.Process(ctx.clientConn, ctx.telegramConn); err != nil { if err := rel.Process(ctx.clientConn, ctx.telegramConn); err != nil {
@@ -122,38 +123,52 @@ func (p *Proxy) Shutdown() {
p.workerPool.Release() p.workerPool.Release()
} }
func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) error { func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
rec := record.AcquireRecord() rec := record.AcquireRecord()
defer record.ReleaseRecord(rec) defer record.ReleaseRecord(rec)
if err := rec.Read(ctx.clientConn); err != nil { rewind := newConnRewind(ctx.clientConn)
return fmt.Errorf("cannot read client hello: %w", err)
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()) hello, err := faketls.ParseClientHello(p.secret.Key[:], rec.Payload.Bytes())
if err != nil { 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 { 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) { 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) p.logger.InfoError("cannot send welcome packet", err)
return errCannotSendWelcomePacket return false
} }
ctx.clientConn = &faketls.Conn{ ctx.clientConn = &faketls.Conn{
Conn: ctx.clientConn, Conn: ctx.clientConn,
} }
return nil return true
} }
func (p *Proxy) doObfuscated2Handshake(ctx *streamContext) error { func (p *Proxy) doObfuscated2Handshake(ctx *streamContext) error {
@@ -207,6 +222,25 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error {
return nil 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 func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop, funlen
switch { switch {
case opts.Network == nil: case opts.Network == nil:
@@ -245,6 +279,11 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop, funlen
bufferSize = DefaultBufferSize bufferSize = DefaultBufferSize
} }
domainFrontingPort := int(opts.DomainFrontingPort)
if domainFrontingPort == 0 {
domainFrontingPort = DefaultDomainFrontingPort
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
proxy := &Proxy{ proxy := &Proxy{
ctx: ctx, ctx: ctx,
@@ -256,9 +295,11 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { // nolint: cyclop, funlen
ipBlocklist: opts.IPBlocklist, ipBlocklist: opts.IPBlocklist,
eventStream: opts.EventStream, eventStream: opts.EventStream,
logger: opts.Logger.Named("proxy"), logger: opts.Logger.Named("proxy"),
idleTimeout: idleTimeout, domainFrontAddress: net.JoinHostPort(opts.Secret.Host,
bufferSize: int(bufferSize), strconv.Itoa(domainFrontingPort)),
telegram: tg, idleTimeout: idleTimeout,
bufferSize: int(bufferSize),
telegram: tg,
} }
pool, err := ants.NewPoolWithFunc(int(concurrency), func(arg interface{}) { pool, err := ants.NewPoolWithFunc(int(concurrency), func(arg interface{}) {
+5 -5
View File
@@ -11,9 +11,9 @@ type ProxyOpts struct {
EventStream EventStream EventStream EventStream
Logger Logger Logger Logger
BufferSize uint BufferSize uint
Concurrency uint Concurrency uint
CloakPort uint DomainFrontingPort uint
IdleTimeout time.Duration IdleTimeout time.Duration
PreferIP string PreferIP string
} }