diff --git a/antireplay/init.go b/antireplay/init.go new file mode 100644 index 0000000..4585a65 --- /dev/null +++ b/antireplay/init.go @@ -0,0 +1,6 @@ +package antireplay + +const ( + DefaultMaxSize = 10 * 1024 * 1024 // 10mib + DefaultErrorRate = 0.0001 +) diff --git a/cli/cli.go b/cli/cli.go index f620e21..d2d813c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -5,5 +5,6 @@ import "github.com/alecthomas/kong" type CLI struct { GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"` Access Access `kong:"cmd,help='Print access information.'"` + Run Proxy `kong:"cmd,help='Run proxy.'"` Version kong.VersionFlag `kong:"help='Print version.',short='v'"` } diff --git a/cli/proxy.go b/cli/proxy.go new file mode 100644 index 0000000..eaf7035 --- /dev/null +++ b/cli/proxy.go @@ -0,0 +1,205 @@ +package cli + +import ( + "fmt" + "net" + "os" + + "github.com/9seconds/mtg/v2/antireplay" + "github.com/9seconds/mtg/v2/events" + "github.com/9seconds/mtg/v2/ipblocklist" + "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/9seconds/mtg/v2/utils" + "github.com/rs/zerolog" +) + +type Proxy struct { + base + + prometheusListener net.Listener + prometheus *stats.PrometheusFactory + statsdFactory *stats.StatsdFactory +} + +func (c *Proxy) Run(cli *CLI, version string) error { + if err := c.ReadConfig(version); err != nil { + return fmt.Errorf("cannot init config: %w", err) + } + + return c.Execute() +} + +func (c *Proxy) Execute() error { // nolint: funlen + zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs + zerolog.TimestampFieldName = "timestamp" + zerolog.LevelFieldName = "level" + + 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(), + + 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), + } + + defer func() { + opts.AntiReplayCache.Shutdown() + opts.IPBlocklist.Shutdown() + opts.EventStream.Shutdown() + }() + + if opts.Concurrency == 0 { + opts.Concurrency = mtglib.DefaultConcurrency + } + + 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) + } + + if err := c.setupEventStream(&opts); err != nil { + return fmt.Errorf("cannot setup event stream: %w", err) + } + + proxy, err := mtglib.NewProxy(opts) + if err != nil { + return fmt.Errorf("cannot create a proxy: %w", err) + } + + listener, err := net.Listen("tcp", c.Config.BindTo.String()) + if err != nil { + return fmt.Errorf("cannot start proxy: %w", err) + } + + go proxy.Serve(listener) // nolint: errcheck + + <-ctx.Done() + + listener.Close() + + if c.prometheusListener != nil { + c.prometheusListener.Close() + } + + if c.prometheus != nil { + c.prometheus.Close() + } + + if c.statsdFactory != nil { + c.statsdFactory.Close() + } + + return nil +} + +func (c *Proxy) setupAntiReplayCache(opts *mtglib.ProxyOpts) { + if !c.Config.Defense.AntiReplay.Enabled { + return + } + + opts.AntiReplayCache = antireplay.NewStableBloomFilter( + c.Config.Defense.AntiReplay.MaxSize.Value(antireplay.DefaultMaxSize), + c.Config.Defense.AntiReplay.ErrorRate.Value(antireplay.DefaultErrorRate), + ) +} + +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 + } + + remoteURLs := []string{} + localFiles := []string{} + + for _, v := range c.Config.Defense.Blocklist.URLs { + if v.IsRemote() { + remoteURLs = append(remoteURLs, v.String()) + } else { + localFiles = append(localFiles, v.String()) + } + } + + firehol, err := ipblocklist.NewFirehol(opts.Logger.Named("ipblockist"), + c.Network, + c.Config.Defense.Blocklist.DownloadConcurrency, + remoteURLs, + localFiles) + if err != nil { + return err // nolint: wrapcheck + } + + go firehol.Run(c.Config.Defense.Blocklist.UpdateEach.Value(ipblocklist.DefaultUpdateEach)) + + opts.IPBlocklist = firehol + + return nil +} + +func (c *Proxy) setupEventStream(opts *mtglib.ProxyOpts) error { + factories := make([]events.ObserverFactory, 0, 2) + + if c.Config.Stats.StatsD.Enabled { + statsdFactory, err := stats.NewStatsd( + c.Config.Stats.StatsD.Address.String(), + opts.Logger.Named("statsd"), + c.Config.Stats.StatsD.MetricPrefix.Value(stats.DefaultStatsdMetricPrefix), + c.Config.Stats.StatsD.TagFormat.Value(stats.DefaultStatsdTagFormat)) + if err != nil { + return fmt.Errorf("cannot build statsd observer: %w", err) + } + + c.statsdFactory = &statsdFactory + + factories = append(factories, statsdFactory.Make) + } + + if c.Config.Stats.Prometheus.Enabled { + prometheus := stats.NewPrometheus( + c.Config.Stats.Prometheus.MetricPrefix.Value(stats.DefaultMetricPrefix), + c.Config.Stats.Prometheus.HTTPPath.Value("/"), + ) + + listener, err := net.Listen("tcp", c.Config.Stats.Prometheus.BindTo.String()) + if err != nil { + return fmt.Errorf("cannot start a listener for prometheus: %w", err) + } + + go prometheus.Serve(listener) // nolint: errcheck + + c.prometheusListener = listener + c.prometheus = prometheus + + factories = append(factories, prometheus.Make) + } + + if len(factories) > 0 { + opts.EventStream = events.NewEventStream(factories) + } + + return nil +} diff --git a/ipblocklist/firehol.go b/ipblocklist/firehol.go index 2d8a748..49f1be7 100644 --- a/ipblocklist/firehol.go +++ b/ipblocklist/firehol.go @@ -323,7 +323,7 @@ func NewFirehol(logger mtglib.Logger, network mtglib.Network, } if downloadConcurrency == 0 { - downloadConcurrency = 1 + downloadConcurrency = DefaultDownloadConcurrency } workerPool, _ := ants.NewPool(int(downloadConcurrency)) diff --git a/ipblocklist/init.go b/ipblocklist/init.go new file mode 100644 index 0000000..cfa8df8 --- /dev/null +++ b/ipblocklist/init.go @@ -0,0 +1,8 @@ +package ipblocklist + +import "time" + +const ( + DefaultDownloadConcurrency = 1 + DefaultUpdateEach = 12 * time.Hour +) diff --git a/main.go b/main.go index e74a760..d554166 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "time" "github.com/9seconds/mtg/v2/cli" + "github.com/9seconds/mtg/v2/utils" "github.com/alecthomas/kong" ) @@ -13,6 +14,10 @@ var version = "dev" // has to be set by ldflags func main() { rand.Seed(time.Now().UTC().UnixNano()) + if err := utils.SetLimits(); err != nil { + panic(err) + } + cli := &cli.CLI{} ctx := kong.Parse(cli, kong.Vars{ "version": version, diff --git a/mtglib/internal/obfuscated2/client_handshake.go b/mtglib/internal/obfuscated2/client_handshake.go index f5e692f..eb6a5a8 100644 --- a/mtglib/internal/obfuscated2/client_handshake.go +++ b/mtglib/internal/obfuscated2/client_handshake.go @@ -9,7 +9,7 @@ import ( ) // Connection Type secure. We support only fake tls. -var clientHandshakeMagic = []byte{0xdd, 0xdd, 0xdd, 0xdd} +var clientHandshakeConnectionType = []byte{0xdd, 0xdd, 0xdd, 0xdd} func ClientHandshake(secret []byte, reader io.Reader) (int16, cipher.Stream, cipher.Stream, error) { handshakeFrame := acquireHandshakeFrame() @@ -42,8 +42,8 @@ func ClientHandshake(secret []byte, reader io.Reader) (int16, cipher.Stream, cip decryptor.XORKeyStream(handshakeFrame.data[:], handshakeFrame.data[:]) - if magic := handshakeFrame.magic(); subtle.ConstantTimeCompare(clientHandshakeMagic, magic) != 1 { - return 0, nil, nil, fmt.Errorf("unsupported connection type: %s", hex.EncodeToString(magic)) + if val := handshakeFrame.connectionType(); subtle.ConstantTimeCompare(clientHandshakeConnectionType, val) != 1 { + return 0, nil, nil, fmt.Errorf("unsupported connection type: %s", hex.EncodeToString(val)) } return handshakeFrame.dc(), encryptor, decryptor, nil diff --git a/mtglib/internal/obfuscated2/handshake_frame.go b/mtglib/internal/obfuscated2/handshake_frame.go index f84a07d..71a993c 100644 --- a/mtglib/internal/obfuscated2/handshake_frame.go +++ b/mtglib/internal/obfuscated2/handshake_frame.go @@ -5,17 +5,17 @@ import "encoding/binary" const ( handshakeFrameLen = 64 - handshakeFrameLenKey = 32 - handshakeFrameLenIV = 16 - handshakeFrameLenMagic = 4 - handshakeFrameLenDC = 2 + handshakeFrameLenKey = 32 + handshakeFrameLenIV = 16 + handshakeFrameLenConnectionType = 4 + handshakeFrameLenDC = 2 - handshakeFrameOffsetStart = 8 - handshakeFrameOffsetKey = handshakeFrameOffsetStart - handshakeFrameOffsetIV = handshakeFrameOffsetKey + handshakeFrameLenKey - handshakeFrameOffsetMagic = handshakeFrameOffsetIV + handshakeFrameLenIV - handshakeFrameOffsetDC = handshakeFrameOffsetMagic + handshakeFrameLenMagic - handshakeFrameOffsetEnd = handshakeFrameOffsetDC + handshakeFrameLenDC + handshakeFrameOffsetStart = 8 + handshakeFrameOffsetKey = handshakeFrameOffsetStart + handshakeFrameOffsetIV = handshakeFrameOffsetKey + handshakeFrameLenKey + handshakeFrameOffsetConnectionType = handshakeFrameOffsetIV + handshakeFrameLenIV + handshakeFrameOffsetDC = handshakeFrameOffsetConnectionType + handshakeFrameLenConnectionType + handshakeFrameOffsetEnd = handshakeFrameOffsetDC + handshakeFrameLenDC ) // A structure of obfuscated2 handshake frame is following: @@ -25,7 +25,7 @@ const ( // - 8 bytes of noise // - 32 bytes of AES Key // - 16 bytes of AES IV -// - 4 bytes of 'magic' - this has some settings like a connection type +// - 4 bytes of 'connection type' - this has some setting like a connection type // - 2 bytes of 'DC'. DC is little endian int16 // - 2 bytes of noise type handshakeFrame struct { @@ -39,13 +39,13 @@ func (h *handshakeFrame) dc() int16 { } func (h *handshakeFrame) key() []byte { - return h.data[handshakeFrameLenKey:handshakeFrameOffsetIV] + return h.data[handshakeFrameOffsetKey:handshakeFrameOffsetIV] } func (h *handshakeFrame) iv() []byte { - return h.data[handshakeFrameOffsetIV:handshakeFrameOffsetMagic] + return h.data[handshakeFrameOffsetIV:handshakeFrameOffsetConnectionType] } -func (h *handshakeFrame) magic() []byte { - return h.data[handshakeFrameOffsetMagic:handshakeFrameOffsetDC] +func (h *handshakeFrame) connectionType() []byte { + return h.data[handshakeFrameOffsetConnectionType:handshakeFrameOffsetDC] } diff --git a/stats/init.go b/stats/init.go index 063eb32..7d095c5 100644 --- a/stats/init.go +++ b/stats/init.go @@ -1,6 +1,11 @@ package stats const ( + DefaultMetricPrefix = "mtg" + + DefaultStatsdMetricPrefix = DefaultMetricPrefix + "." + DefaultStatsdTagFormat = "datadog" + MetricActiveConnection = "active_connections" MetricSessionDuration = "session_duration" MetricConcurrencyLimited = "concurrency_limited" diff --git a/timeattack/init.go b/timeattack/init.go new file mode 100644 index 0000000..385b9d7 --- /dev/null +++ b/timeattack/init.go @@ -0,0 +1,7 @@ +package timeattack + +import "time" + +const ( + DefaultDuration = time.Second +) diff --git a/utils/rlimit.go b/utils/rlimit.go new file mode 100644 index 0000000..b474076 --- /dev/null +++ b/utils/rlimit.go @@ -0,0 +1,24 @@ +// +build !windows + +package utils + +import ( + "fmt" + + "golang.org/x/sys/unix" +) + +func SetLimits() error { + rLimit := unix.Rlimit{} + if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil { + return fmt.Errorf("cannot get rlimit: %w", err) + } + + rLimit.Cur = rLimit.Max + + if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil { + return fmt.Errorf("cannot set rlimit: %w", err) + } + + return nil +} diff --git a/utils/rlimit_windows.go b/utils/rlimit_windows.go new file mode 100644 index 0000000..58bdf39 --- /dev/null +++ b/utils/rlimit_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package utils + +func SetLimits() error { + return nil +} diff --git a/utils/root_context.go b/utils/root_context.go new file mode 100644 index 0000000..9fe4b49 --- /dev/null +++ b/utils/root_context.go @@ -0,0 +1,25 @@ +// +build !windows + +package utils + +import ( + "context" + "os" + "os/signal" + "syscall" +) + +func RootContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + sigChan := make(chan os.Signal, 1) + + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + go func() { + for range sigChan { + cancel() + } + }() + + return ctx +} diff --git a/utils/root_context_windows.go b/utils/root_context_windows.go new file mode 100644 index 0000000..9a5b6e0 --- /dev/null +++ b/utils/root_context_windows.go @@ -0,0 +1,23 @@ +// +build windows + +package utils + +import ( + "context" + "os" + "os/signal" +) + +func RootContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + sigChan := make(chan os.Signal, 1) + + signal.Notify(sigChan, os.Interrupt) + go func() { + for range sigChan { + cancel() + } + }() + + return ctx +}