mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 20:34:02 +03:00
Implement domain fronting
This commit is contained in:
+5
-5
@@ -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
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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{}) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user