Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ Flags:
-n, --doh-ip=1.1.1.1 IP address of DNS-over-HTTP to use.
-t, --timeout=10s Network timeout to use
-a, --antireplay-cache-size="1MB" A size of anti-replay cache to use.
--dpi-desync Enable Linux IPv4 DPI desync for fake TLS handshakes.
```

So, if you want to startup a proxy with CLI only, you can do something like
Expand Down Expand Up @@ -401,6 +402,10 @@ bind-to = "0.0.0.0:443"
This is enough to run the whole application. All other
options already have sensible defaults for the app at almost any scale.

`dpi-desync = true` enables Linux IPv4-only DPI desync for FakeTLS
handshakes. It opens a raw packet socket, so run mtg as root or grant
`CAP_NET_RAW`.

### Run a proxy

Put a binary and a config into your webserver. Just for example,
Expand All @@ -422,6 +427,8 @@ RestartSec=3
DynamicUser=true
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
# If dpi-desync = true, also add CAP_NET_RAW:
# AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW

[Install]
WantedBy=multi-user.target
Expand Down
16 changes: 16 additions & 0 deletions essentials/conns.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package essentials

import (
"fmt"
"io"
"net"
"syscall"
)

// CloseableReader is an [io.Reader] interface that can close its reading end.
Expand Down Expand Up @@ -49,3 +51,17 @@ func (n netConnWrapper) CloseWrite() error {
func WrapNetConn(conn net.Conn) Conn {
return netConnWrapper{conn}
}

func SetTCPWindowClamp(conn net.Conn, value int) error {
sysConn, ok := conn.(syscall.Conn)
if !ok {
return nil
}

rawConn, err := sysConn.SyscallConn()
if err != nil {
return fmt.Errorf("cannot get raw connection: %w", err)
}

return SetRawTCPWindowClamp(rawConn, value)
}
25 changes: 25 additions & 0 deletions essentials/tcp_options_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build linux

package essentials

import (
"fmt"
"syscall"

"golang.org/x/sys/unix"
)

func SetRawTCPWindowClamp(conn syscall.RawConn, value int) error {
var err error

if controlErr := conn.Control(func(fd uintptr) {
err = unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_WINDOW_CLAMP, value)
}); controlErr != nil && err == nil {
return fmt.Errorf("cannot access TCP socket: %w", controlErr)
}
if err != nil {
return fmt.Errorf("cannot set TCP_WINDOW_CLAMP: %w", err)
}

return nil
}
9 changes: 9 additions & 0 deletions essentials/tcp_options_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !linux

package essentials

import "syscall"

func SetRawTCPWindowClamp(_ syscall.RawConn, _ int) error {
return nil
}
6 changes: 6 additions & 0 deletions example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ bind-to = "0.0.0.0:3128"
# default value is false.
# proxy-protocol-listener = false

# Enable Linux IPv4 DPI desync for FakeTLS handshakes. This opens a raw packet
# socket, so the process needs root or CAP_NET_RAW. The option is ignored on
# non-Linux builds.
# default value is false.
# dpi-desync = false

# Defines how many concurrent connections are allowed to this proxy.
# All other incoming connections are going to be dropped.
concurrency = 8192
Expand Down
28 changes: 25 additions & 3 deletions internal/cli/run_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/9seconds/mtg/v2/antireplay"
"github.com/9seconds/mtg/v2/events"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/desync"
"github.com/9seconds/mtg/v2/internal/proxyprotocol"
"github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/ipblocklist"
Expand Down Expand Up @@ -179,7 +180,8 @@ func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStr
conf.Stats.StatsD.Address.Get(""),
logger.Named("statsd"),
conf.Stats.StatsD.MetricPrefix.Get(stats.DefaultStatsdMetricPrefix),
conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat))
conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat),
)
if err != nil {
return nil, fmt.Errorf("cannot build statsd observer: %w", err)
}
Expand Down Expand Up @@ -254,6 +256,8 @@ func warnDeprecatedDomainFronting(conf *config.Config, log mtglib.Logger) {
}
}

const dpiDesyncHandshakeWindowClamp = 256

func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyclop
logger := makeLogger(conf)

Expand All @@ -279,7 +283,8 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
ntw,
func(ctx context.Context, size int) {
eventStream.Send(ctx, mtglib.NewEventIPListSize(size, true))
})
},
)
if err != nil {
return fmt.Errorf("cannot build ip blocklist: %w", err)
}
Expand All @@ -296,6 +301,13 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
return fmt.Errorf("cannot build ip allowlist: %w", err)
}

windowClamp := 0
if conf.DPIDesync.Get(false) {
// Empirically chosen: small enough for Linux IPv4 DPI desync, but still
// large enough for Telegram media after the post-handshake clamp restore.
windowClamp = dpiDesyncHandshakeWindowClamp
}

doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
for i, v := range conf.Defense.Doppelganger.URLs {
doppelGangerURLs[i] = v.String()
Expand Down Expand Up @@ -326,14 +338,16 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
DoppelGangerEach: conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
DoppelGangerDRS: conf.Defense.Doppelganger.DRS.Get(false),

DPIDesync: windowClamp > 0,
}

proxy, err := mtglib.NewProxy(opts)
if err != nil {
return fmt.Errorf("cannot create a proxy: %w", err)
}

listener, err := utils.NewListener(conf.BindTo.Get(""), 0)
listener, err := utils.NewListener(conf.BindTo.Get(""), windowClamp)
if err != nil {
return fmt.Errorf("cannot start proxy: %w", err)
}
Expand All @@ -348,6 +362,14 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc

ctx := utils.RootContext()

if windowClamp > 0 {
desyncSvc, err := desync.Start(int(conf.BindTo.Port))
if err != nil {
return fmt.Errorf("cannot start raw desync: %w", err)
}
defer desyncSvc.Close() //nolint: errcheck
}

go proxy.Serve(listener) //nolint: errcheck

<-ctx.Done()
Expand Down
25 changes: 14 additions & 11 deletions internal/cli/simple_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ type SimpleRun struct {
BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"`

Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` //nolint: lll
Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` //nolint: lll
TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` //nolint: lll
PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` //nolint: lll
DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` //nolint: lll
DomainFrontingHost string `kong:"name='domain-fronting-host',help='Hostname or IP to dial for domain fronting instead of resolving the secret hostname.'"` //nolint: lll
DomainFrontingIP string `kong:"name='domain-fronting-ip',help='Deprecated: use --domain-fronting-host. Setting this flag logs a warning at startup and the value is ignored.'"` //nolint: lll
DOHIP net.IP `kong:"name='doh-ip',short='n',default='1.1.1.1',help='IP address of DNS-over-HTTP to use.'"` //nolint: lll
Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` //nolint: lll
Socks5Proxies []string `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"` //nolint: lll
AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` //nolint: lll
Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` //nolint: lll
Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` //nolint: lll
TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` //nolint: lll
PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` //nolint: lll
DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` //nolint: lll
DomainFrontingHost string `kong:"name='domain-fronting-host',help='Hostname or IP to dial for domain fronting instead of resolving the secret hostname.'"` //nolint: lll
DomainFrontingIP string `kong:"name='domain-fronting-ip',help='Deprecated: use --domain-fronting-host. Setting this flag logs a warning at startup and the value is ignored.'"` //nolint: lll
DOHIP net.IP `kong:"name='doh-ip',short='n',default='1.1.1.1',help='IP address of DNS-over-HTTP to use.'"` //nolint: lll
Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` //nolint: lll
Socks5Proxies []string `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"` //nolint: lll
AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` //nolint: lll

DPIDesync bool `kong:"name='dpi-desync',help='Enable Linux IPv4 DPI desync for fake TLS handshakes.'"` //nolint: lll

ProxyProtocolListener bool `kong:"name='proxy-protocol-listener',help='Expect PROXY protocol (v1 or v2) headers on the listener. Use when mtg sits behind HAProxy, nginx stream, or similar.'"` //nolint: lll
}
Expand Down Expand Up @@ -99,6 +101,7 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { //nolint: cyclop,funle
conf.Debug.Value = s.Debug
conf.AllowFallbackOnUnknownDC.Value = true
conf.Defense.AntiReplay.Enabled.Value = true
conf.DPIDesync.Value = s.DPIDesync
conf.ProxyProtocolListener.Value = s.ProxyProtocolListener

if err := conf.Validate(); err != nil {
Expand Down
9 changes: 5 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Config struct {
Secret mtglib.Secret `json:"secret"`
BindTo TypeHostPort `json:"bindTo"`
ProxyProtocolListener TypeBool `json:"proxyProtocolListener"`
DPIDesync TypeBool `json:"dpiDesync"`
PreferIP TypePreferIP `json:"preferIp"`
AutoUpdate TypeBool `json:"autoUpdate"`
DomainFrontingPort TypePort `json:"domainFrontingPort"`
Expand Down Expand Up @@ -71,10 +72,10 @@ type Config struct {
Interval TypeDuration `json:"interval"`
Count TypeConcurrency `json:"count"`
} `json:"keepAlive"`
DOHIP TypeIP `json:"dohIp"`
DNS TypeDNSURI `json:"dns"`
Proxies []TypeProxyURL `json:"proxies"`
TCPNotSentLowat TypeBytes `json:"tcpNotSentLowat"`
DOHIP TypeIP `json:"dohIp"`
DNS TypeDNSURI `json:"dns"`
Proxies []TypeProxyURL `json:"proxies"`
TCPNotSentLowat TypeBytes `json:"tcpNotSentLowat"`
} `json:"network"`
Stats struct {
StatsD struct {
Expand Down
6 changes: 6 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ func (suite *ConfigTestSuite) TestParsePublicIPNotSet() {
suite.Nil(conf.PublicIPv6.Get(nil))
}

func (suite *ConfigTestSuite) TestParseDPIDesync() {
conf, err := config.Parse(suite.ReadConfig("dpi_desync.toml"))
suite.NoError(err)
suite.True(conf.DPIDesync.Get(false))
}

func (suite *ConfigTestSuite) TestString() {
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
suite.NoError(err)
Expand Down
1 change: 1 addition & 0 deletions internal/config/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type tomlConfig struct {
Secret string `toml:"secret" json:"secret"`
BindTo string `toml:"bind-to" json:"bindTo"`
ProxyProtocolListener bool `toml:"proxy-protocol-listener" json:"proxyProtocolListener"`
DPIDesync bool `toml:"dpi-desync" json:"dpiDesync,omitempty"`
PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"`
AutoUpdate bool `toml:"auto-update" json:"autoUpdate,omitempty"`
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"`
Expand Down
3 changes: 3 additions & 0 deletions internal/config/testdata/dpi_desync.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
dpi-desync = true
Loading
Loading