diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d813b55..ba56441 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,11 +38,11 @@ jobs: test: name: Test runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 strategy: matrix: go_version: - - ^1.17 + - ^1.18 steps: - name: Checkout uses: actions/checkout@v2 @@ -69,6 +69,38 @@ jobs: with: file: ./coverage.txt + fuzz: + name: Fuzzing + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ^1.18 + + - name: Cache fuzz results + uses: actions/cache@v2 + with: + path: ~/.cache/go-build/fuzz + key: ${{ runner.os }}-go-${{ hashFiles('**/*_fuzz_test.go', '**/*_fuzz_internal_test.go') }} + restore-keys: ${{ runner.os }}-go- + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-go- + + - name: Run fuzzing + run: make -j4 fuzz + lint: name: Lint runs-on: ubuntu-latest @@ -79,10 +111,15 @@ jobs: with: submodules: recursive - - name: Run linter - uses: golangci/golangci-lint-action@v2 + - name: Setup Go + uses: actions/setup-go@v2 with: - version: v1.44.2 + go-version: ^1.18 + + - name: Run linter + uses: golangci/golangci-lint-action@v3 + with: + version: v1.45.0 docker: name: Docker diff --git a/.golangci.toml b/.golangci.toml index 353b1d9..a595c75 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -9,4 +9,4 @@ format = "colored-line-number" [linters] enable-all = true -disable = ["ireturn", "varnamelen", "gochecknoglobals", "gas", "goerr113", "exhaustivestruct", "containedctx"] +disable = ["thelper", "ireturn", "varnamelen", "gochecknoglobals", "gas", "goerr113", "exhaustivestruct", "containedctx"] diff --git a/Dockerfile b/Dockerfile index 08003f7..b700b9d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ############################################################################### # BUILD STAGE -FROM golang:1.17-alpine AS build +FROM golang:1.18-alpine AS build RUN set -x \ && apk --no-cache --update add \ diff --git a/Makefile b/Makefile index 354cdec..9a4c7a0 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,12 @@ ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) IMAGE_NAME := mtg APP_NAME := $(IMAGE_NAME) -GOLANGCI_LINT_VERSION := v1.44.2 +GOLANGCI_LINT_VERSION := v1.45.0 -VERSION_GO := $(shell go version) -VERSION_DATE := $(shell date -Ru) -VERSION_TAG := $(shell git describe --tags --always) -COMMON_BUILD_FLAGS := -trimpath -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'" +VERSION := $(shell git describe --exact-match HEAD 2>/dev/null || git describe --tags --always) +COMMON_BUILD_FLAGS := -trimpath -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION)'" + +FUZZ_FLAGS := -fuzztime=120s GOBIN := $(ROOT_DIR)/.bin GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)" @@ -78,7 +78,7 @@ install-tools: install-tools-lint install-tools-godoc install-tools-gofumpt inst .PHONY: install-tools-lint install-tools-lint: .bin - @curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \ + @curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ | bash -s -- -b "$(GOBIN)" "$(GOLANGCI_LINT_VERSION)" .PHONY: install-tools-godoc @@ -95,4 +95,27 @@ install-tools-goreleaser: .bin .PHONY: update-deps update-deps: - @go get -u && go mod tidy -go=1.17 + @go get -u && go mod tidy -go=1.18 + +.PHONY: fuzz +fuzz: fuzz-ClientHello fuzz-ServerGenerateHandshakeFrame fuzz-ClientHandshake fuzz-ServerReceive fuzz-ServerSend + +.PHONY: fuzz-ClientHello +fuzz-ClientHello: + @go test -fuzz=FuzzClientHello $(FUZZ_FLAGS) "$(ROOT_DIR)/mtglib/internal/faketls" + +.PHONY: fuzz-ServerGenerateHandshakeFrame +fuzz-ServerGenerateHandshakeFrame: + @go test -fuzz=FuzzServerGenerateHandshakeFrame $(FUZZ_FLAGS) "$(ROOT_DIR)/mtglib/internal/obfuscated2" + +.PHONY: fuzz-ClientHandshake +fuzz-ClientHandshake: + @go test -fuzz=FuzzClientHandshake $(FUZZ_FLAGS) "$(ROOT_DIR)/mtglib/internal/obfuscated2" + +.PHONY: fuzz-ServerReceive +fuzz-ServerReceive: + @go test -fuzz=FuzzServerReceive $(FUZZ_FLAGS) "$(ROOT_DIR)/mtglib/internal/obfuscated2" + +.PHONY: fuzz-ServerSend +fuzz-ServerSend: + @go test -fuzz=FuzzServerSend $(FUZZ_FLAGS) "$(ROOT_DIR)/mtglib/internal/obfuscated2" diff --git a/README.md b/README.md index 0740b75..15c738f 100644 --- a/README.md +++ b/README.md @@ -317,12 +317,16 @@ Now you can create a systemd unit: ```console $ cat /etc/systemd/system/mtg.service [Unit] -Description=mtg +Description=mtg - MTProto proxy server +Documentation=https://github.com/9seconds/mtg +After=network.target [Service] ExecStart=/usr/local/bin/mtg run /etc/mtg.toml Restart=always RestartSec=3 +DynamicUser=true +AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target @@ -388,7 +392,7 @@ Here goes a list of metrics with their types but without a prefix. | domain_fronting_traffic | counter | `direction` | Count of bytes, transmitted to/from fronting domain. | | domain_fronting | counter | – | Count of domain fronting events. | | concurrency_limited | counter | – | Count of events, when client connection was rejected due to concurrency limit. | -| ip_blocklisted | counter | – | Count of events when client connection was rejected because IP was found in the blacklist. | +| ip_blocklisted | counter | `ip_list` | Count of events when client connection was rejected because IP was found in the blocklist. | | replay_attacks | counter | – | Count of detected replay attacks. | Tag meaning: diff --git a/go.mod b/go.mod index a245cd2..5d27ae8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/9seconds/mtg/v2 -go 1.17 +go 1.18 require ( github.com/OneOfOne/xxhash v1.2.8 @@ -23,9 +23,9 @@ require ( github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.7.0 github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 - golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 + golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index b4021d6..7564015 100644 --- a/go.sum +++ b/go.sum @@ -291,8 +291,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU= -golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -416,11 +416,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index ae06a80..01c6240 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -12,11 +12,13 @@ import ( "github.com/9seconds/mtg/v2/internal/config" "github.com/9seconds/mtg/v2/internal/utils" "github.com/9seconds/mtg/v2/ipblocklist" + "github.com/9seconds/mtg/v2/ipblocklist/files" "github.com/9seconds/mtg/v2/logger" "github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/network" "github.com/9seconds/mtg/v2/stats" "github.com/rs/zerolog" + "github.com/yl2chen/cidranger" ) func makeLogger(conf *config.Config) mtglib.Logger { @@ -89,7 +91,8 @@ func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache { func makeIPBlocklist(conf config.ListConfig, logger mtglib.Logger, ntw mtglib.Network, - updateCallback ipblocklist.FireholUpdateCallback) (mtglib.IPBlocklist, error) { + updateCallback ipblocklist.FireholUpdateCallback, +) (mtglib.IPBlocklist, error) { if !conf.Enabled.Get(false) { return ipblocklist.NewNoop(), nil } @@ -105,7 +108,7 @@ func makeIPBlocklist(conf config.ListConfig, } } - firehol, err := ipblocklist.NewFirehol(logger.Named("ipblockist"), + blocklist, err := ipblocklist.NewFirehol(logger.Named("ipblockist"), ntw, conf.DownloadConcurrency.Get(1), remoteURLs, @@ -115,9 +118,44 @@ func makeIPBlocklist(conf config.ListConfig, return nil, fmt.Errorf("incorrect parameters for firehol: %w", err) } - go firehol.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach)) + go blocklist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach)) - return firehol, nil + return blocklist, nil +} + +func makeIPAllowlist(conf config.ListConfig, + logger mtglib.Logger, + ntw mtglib.Network, + updateCallback ipblocklist.FireholUpdateCallback, +) (allowlist mtglib.IPBlocklist, err error) { + if !conf.Enabled.Get(false) { + allowlist, err = ipblocklist.NewFireholFromFiles( + logger.Named("ipblocklist"), + 1, + []files.File{ + files.NewMem([]*net.IPNet{ + cidranger.AllIPv4, + cidranger.AllIPv6, + }), + }, + updateCallback, + ) + + go allowlist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach)) + } else { + allowlist, err = makeIPBlocklist( + conf, + logger, + ntw, + updateCallback, + ) + } + + if err != nil { + return nil, fmt.Errorf("cannot build allowlist: %w", err) + } + + return allowlist, nil } func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStream, error) { @@ -185,21 +223,16 @@ func runProxy(conf *config.Config, version string) error { // nolint: funlen return fmt.Errorf("cannot build ip blocklist: %w", err) } - var whitelist mtglib.IPBlocklist - - if conf.Defense.Allowlist.Enabled.Get(false) { - whlist, err := makeIPBlocklist( - conf.Defense.Allowlist, - logger.Named("allowlist"), - ntw, - func(ctx context.Context, size int) { - eventStream.Send(ctx, mtglib.NewEventIPListSize(size, false)) - }) - if err != nil { - return fmt.Errorf("cannot build ip allowlist: %w", err) - } - - whitelist = whlist + allowlist, err := makeIPAllowlist( + conf.Defense.Allowlist, + logger.Named("allowlist"), + ntw, + func(ctx context.Context, size int) { + eventStream.Send(ctx, mtglib.NewEventIPListSize(size, false)) + }, + ) + if err != nil { + return fmt.Errorf("cannot build ip allowlist: %w", err) } opts := mtglib.ProxyOpts{ @@ -207,7 +240,7 @@ func runProxy(conf *config.Config, version string) error { // nolint: funlen Network: ntw, AntiReplayCache: makeAntiReplayCache(conf), IPBlocklist: blocklist, - IPWhitelist: whitelist, + IPAllowlist: allowlist, EventStream: eventStream, Secret: conf.Secret, diff --git a/internal/testlib/mtglib_network_mock.go b/internal/testlib/mtglib_network_mock.go index a6d08bf..7eb21a4 100644 --- a/internal/testlib/mtglib_network_mock.go +++ b/internal/testlib/mtglib_network_mock.go @@ -25,6 +25,7 @@ func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address st } func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context, - network, address string) (essentials.Conn, error)) *http.Client { + network, address string) (essentials.Conn, error), +) *http.Client { return m.Called(dialFunc).Get(0).(*http.Client) // nolint: forcetypeassert } diff --git a/ipblocklist/files/mem.go b/ipblocklist/files/mem.go new file mode 100644 index 0000000..db23bef --- /dev/null +++ b/ipblocklist/files/mem.go @@ -0,0 +1,37 @@ +package files + +import ( + "context" + "io" + "net" + "strings" +) + +type memFile struct { + data string +} + +func (m memFile) Open(ctx context.Context) (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(m.data)), nil +} + +func (m memFile) String() string { + return "mem" +} + +func NewMem(networks []*net.IPNet) File { + builder := strings.Builder{} + + if len(networks) > 0 { + builder.WriteString(networks[0].String()) + } + + for i := 1; i < len(networks); i++ { + builder.WriteString("\n") + builder.WriteString(networks[i].String()) + } + + return memFile{ + data: builder.String(), + } +} diff --git a/ipblocklist/files/mem_test.go b/ipblocklist/files/mem_test.go new file mode 100644 index 0000000..e3142b2 --- /dev/null +++ b/ipblocklist/files/mem_test.go @@ -0,0 +1,42 @@ +package files_test + +import ( + "context" + "io" + "net" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/ipblocklist/files" + "github.com/stretchr/testify/suite" +) + +type MemTestSuite struct { + suite.Suite +} + +func (suite *MemTestSuite) TestOk() { + _, network1, _ := net.ParseCIDR("192.168.0.1/24") + _, network2, _ := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/36") + + file := files.NewMem([]*net.IPNet{ + network1, + network2, + }) + + reader, err := file.Open(context.Background()) + suite.NoError(err) + + data, err := io.ReadAll(reader) + suite.NoError(err) + + strData := strings.TrimSpace(string(data)) + + suite.Contains(strData, "192.168.0.0/24") + suite.Contains(strData, "2001:db8:8000::/36") +} + +func TestMem(t *testing.T) { + t.Parallel() + suite.Run(t, &MemTestSuite{}) +} diff --git a/ipblocklist/firehol.go b/ipblocklist/firehol.go index d6fa3e8..efe57f0 100644 --- a/ipblocklist/firehol.go +++ b/ipblocklist/firehol.go @@ -155,7 +155,8 @@ func (f *Firehol) update() { func (f *Firehol) updateFromFile(mutex sync.Locker, ranger cidranger.Ranger, - scanner *bufio.Scanner) error { + scanner *bufio.Scanner, +) error { for scanner.Scan() { text := scanner.Text() text = fireholRegexpComment.ReplaceAllLiteralString(text, "") @@ -216,7 +217,8 @@ func NewFirehol(logger mtglib.Logger, network mtglib.Network, downloadConcurrency uint, urls []string, localFiles []string, - updateCallback FireholUpdateCallback) (*Firehol, error) { + updateCallback FireholUpdateCallback, +) (*Firehol, error) { blocklists := []files.File{} for _, v := range localFiles { @@ -245,7 +247,8 @@ func NewFirehol(logger mtglib.Logger, network mtglib.Network, func NewFireholFromFiles(logger mtglib.Logger, downloadConcurrency uint, blocklists []files.File, - updateCallback FireholUpdateCallback) (*Firehol, error) { + updateCallback FireholUpdateCallback, +) (*Firehol, error) { if downloadConcurrency == 0 { downloadConcurrency = DefaultFireholDownloadConcurrency } diff --git a/main.go b/main.go index 019278b..dd795c7 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,10 @@ package main import ( + "fmt" "math/rand" + "runtime/debug" + "strconv" "time" "github.com/9seconds/mtg/v2/internal/cli" @@ -26,6 +29,32 @@ func main() { panic(err) } + if buildInfo, ok := debug.ReadBuildInfo(); ok { + vcsCommit := "" + vcsDate := time.Now() + vcsDirty := "" + + for _, setting := range buildInfo.Settings { + switch setting.Key { + case "vcs.time": + vcsDate, _ = time.Parse(time.RFC3339, setting.Value) + case "vcs.revision": + vcsCommit = setting.Value + case "vcs.modified": + if isDirty, _ := strconv.ParseBool(setting.Value); isDirty { + vcsDirty = " [dirty]" + } + } + } + + version = fmt.Sprintf("%s (%s: %s on %s%s)", + version, + buildInfo.GoVersion, + vcsDate.Format(time.RFC3339), + vcsCommit, + vcsDirty) + } + cli := &cli.CLI{} ctx := kong.Parse(cli, kong.Vars{ "version": version, diff --git a/mtglib/events.go b/mtglib/events.go index fdc387b..f1c5152 100644 --- a/mtglib/events.go +++ b/mtglib/events.go @@ -83,7 +83,8 @@ type EventConcurrencyLimited struct { type EventIPBlocklisted struct { eventBase - RemoteIP net.IP + RemoteIP net.IP + IsBlockList bool } // EventReplayAttack is emitted when mtg detects a replay attack on a @@ -172,7 +173,20 @@ func NewEventIPBlocklisted(remoteIP net.IP) EventIPBlocklisted { eventBase: eventBase{ timestamp: time.Now(), }, - RemoteIP: remoteIP, + RemoteIP: remoteIP, + IsBlockList: true, + } +} + +// NewEventIPAllowlisted creates a NewEventIPBlocklisted event with a mark that +// it is supposed to be for allow list. +func NewEventIPAllowlisted(remoteIP net.IP) EventIPBlocklisted { + return EventIPBlocklisted{ + eventBase: eventBase{ + timestamp: time.Now(), + }, + RemoteIP: remoteIP, + IsBlockList: false, } } diff --git a/mtglib/events_test.go b/mtglib/events_test.go index 56da979..ddce9cc 100644 --- a/mtglib/events_test.go +++ b/mtglib/events_test.go @@ -60,6 +60,15 @@ func (suite *EventsTestSuite) TestEventIPBlocklisted() { suite.Empty(evt.StreamID()) suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) + suite.True(evt.IsBlockList) +} + +func (suite *EventsTestSuite) TestEventIPAllowlisted() { + evt := mtglib.NewEventIPAllowlisted(net.ParseIP("10.0.0.10")) + + suite.Empty(evt.StreamID()) + suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) + suite.False(evt.IsBlockList) } func (suite *EventsTestSuite) TestEventReplayAttack() { diff --git a/mtglib/init.go b/mtglib/init.go index a245935..0e00db6 100644 --- a/mtglib/init.go +++ b/mtglib/init.go @@ -48,6 +48,10 @@ var ( // create a proxy but ip blocklist instance is not defined. ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined") + // ErrIPAllowlistIsNotDefined is returned if you are trying to + // create a proxy but ip allowlist instance is not defined. + ErrIPAllowlistIsNotDefined = errors.New("ip allowlist is not defined") + // ErrEventStreamIsNotDefined is returned if you are trying to create a // proxy but event stream instance is not defined. ErrEventStreamIsNotDefined = errors.New("event stream is not defined") diff --git a/mtglib/internal/faketls/client_hello_fuzz_test.go b/mtglib/internal/faketls/client_hello_fuzz_test.go new file mode 100644 index 0000000..84db20d --- /dev/null +++ b/mtglib/internal/faketls/client_hello_fuzz_test.go @@ -0,0 +1,21 @@ +package faketls_test + +import ( + "testing" + + "github.com/9seconds/mtg/v2/mtglib/internal/faketls" + "github.com/stretchr/testify/require" +) + +var FuzzClientHelloSecret = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + +func FuzzClientHello(f *testing.F) { + f.Add([]byte{1, 2, 3}) + + f.Fuzz(func(t *testing.T, frame []byte) { + _, err := faketls.ParseClientHello(FuzzClientHelloSecret, frame) + + // a probability of having != err is almost negligible + require.Error(t, err) + }) +} diff --git a/mtglib/internal/faketls/init.go b/mtglib/internal/faketls/init.go index 3a305a5..b514b21 100644 --- a/mtglib/internal/faketls/init.go +++ b/mtglib/internal/faketls/init.go @@ -19,7 +19,7 @@ const ( // ClientHelloMinLen is a minimal possible length of // ClientHello record. - ClientHelloMinLen = 4 + ClientHelloMinLen = 6 // WelcomePacketRandomOffset is an offset of random in ServerHello // packet (including record envelope). diff --git a/mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go b/mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go new file mode 100644 index 0000000..01a8a1e --- /dev/null +++ b/mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go @@ -0,0 +1,32 @@ +package obfuscated2 + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +var FuzzClientHandshakeSecret = []byte{1, 2, 3} + +func FuzzClientHandshake(f *testing.F) { + f.Add([]byte{1, 2, 3}) + + f.Fuzz(func(t *testing.T, frame []byte) { + data := bytes.NewReader(frame) + + if _, _, _, err := ClientHandshake(FuzzClientHandshakeSecret, data); err != nil { + return + } + + handshake := clientHandhakeFrame{} + require.Len(t, frame, handshakeFrameLen) + + copy(handshake.data[:], frame) + + decryptor := handshake.decryptor(FuzzClientHandshakeSecret) + decryptor.XORKeyStream(handshake.data[:], handshake.data[:]) + + require.Equal(t, handshakeConnectionType, handshake.connectionType()) + }) +} diff --git a/mtglib/internal/obfuscated2/init_test.go b/mtglib/internal/obfuscated2/init_test.go index 001c84b..50d031c 100644 --- a/mtglib/internal/obfuscated2/init_test.go +++ b/mtglib/internal/obfuscated2/init_test.go @@ -1,12 +1,20 @@ package obfuscated2_test import ( + "bytes" + "crypto/aes" + "crypto/cipher" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/testlib" + "github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2" + "github.com/stretchr/testify/require" ) type snapshotBytes struct { @@ -50,6 +58,14 @@ type SnapshotTestSuite struct { snapshots map[string]*Obfuscated2Snapshot } +type ServerHandshakeTestData struct { + connMock *testlib.EssentialsConnMock + + proxyConn obfuscated2.Conn + encryptor cipher.Stream + decryptor cipher.Stream +} + func (suite *SnapshotTestSuite) IngestSnapshots(dirname, namePrefix string) error { suite.snapshots = map[string]*Obfuscated2Snapshot{} @@ -81,3 +97,41 @@ func (suite *SnapshotTestSuite) IngestSnapshots(dirname, namePrefix string) erro return nil } + +func NewServerHandshakeTestData(t *testing.T) ServerHandshakeTestData { + buf := &bytes.Buffer{} + connMock := &testlib.EssentialsConnMock{} + + handshakeEnc, handshakeDec, err := obfuscated2.ServerHandshake(buf) + require.NoError(t, err) + + serverEncrypted := buf.Bytes() + decBlock, _ := aes.NewCipher(serverEncrypted[8 : 8+32]) + decryptor := cipher.NewCTR(decBlock, serverEncrypted[8+32:8+32+16]) + + serverDecrypted := make([]byte, len(serverEncrypted)) + decryptor.XORKeyStream(serverDecrypted, serverEncrypted) + + require.Equal(t, "3d3d3Q", + base64.RawStdEncoding.EncodeToString(serverDecrypted[8+32+16:8+32+16+4])) + + serverEncryptedReverted := make([]byte, len(serverEncrypted)) + + for i := 0; i < 32+16; i++ { + serverEncryptedReverted[8+i] = serverEncrypted[8+32+16-1-i] + } + + encBlock, _ := aes.NewCipher(serverEncryptedReverted[8 : 8+32]) + encryptor := cipher.NewCTR(encBlock, serverEncryptedReverted[8+32:8+32+16]) + + return ServerHandshakeTestData{ + connMock: connMock, + proxyConn: obfuscated2.Conn{ + Conn: connMock, + Encryptor: handshakeEnc, + Decryptor: handshakeDec, + }, + encryptor: encryptor, + decryptor: decryptor, + } +} diff --git a/mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go b/mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go new file mode 100644 index 0000000..125fd9c --- /dev/null +++ b/mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go @@ -0,0 +1,30 @@ +package obfuscated2 + +import ( + "encoding/binary" + "testing" + + "github.com/stretchr/testify/assert" +) + +func FuzzServerGenerateHandshakeFrame(f *testing.F) { + f.Fuzz(func(t *testing.T, arg int) { + frame := generateServerHanshakeFrame() + + assert.NotEqualValues(t, 0xef, frame.data[0]) + + firstBytes := binary.LittleEndian.Uint32(frame.data[:4]) + assert.NotEqualValues(t, 0x44414548, firstBytes) + assert.NotEqualValues(t, 0x54534f50, firstBytes) + assert.NotEqualValues(t, 0x20544547, firstBytes) + assert.NotEqualValues(t, 0x4954504f, firstBytes) + assert.NotEqualValues(t, 0xeeeeeeee, firstBytes) + + assert.NotEqualValues( + t, + 0, + frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7]) + + assert.Equal(t, handshakeConnectionType, frame.connectionType()) + }) +} diff --git a/mtglib/internal/obfuscated2/server_handshake_fuzz_test.go b/mtglib/internal/obfuscated2/server_handshake_fuzz_test.go new file mode 100644 index 0000000..d129f37 --- /dev/null +++ b/mtglib/internal/obfuscated2/server_handshake_fuzz_test.go @@ -0,0 +1,58 @@ +package obfuscated2_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func FuzzServerSend(f *testing.F) { + f.Add([]byte{1, 2, 3, 4, 5}) + + f.Fuzz(func(t *testing.T, data []byte) { + handshakeData := NewServerHandshakeTestData(t) + + handshakeData.connMock. + On("Write", mock.Anything). + Return(len(data), nil). + Once(). + Run(func(args mock.Arguments) { + message := make([]byte, len(data)) + handshakeData.decryptor.XORKeyStream(message, args.Get(0).([]byte)) // nolint: forcetypeassert + assert.Equal(t, message, data) + }) + + n, err := handshakeData.proxyConn.Write(data) + + assert.EqualValues(t, len(data), n) + assert.NoError(t, err) + handshakeData.connMock.AssertExpectations(t) + }) +} + +func FuzzServerReceive(f *testing.F) { + f.Add([]byte{1, 2, 3, 4, 5}) + + f.Fuzz(func(t *testing.T, data []byte) { + handshakeData := NewServerHandshakeTestData(t) + buffer := make([]byte, len(data)) + + handshakeData.connMock. + On("Read", mock.Anything). + Return(len(data), nil). + Once(). + Run(func(args mock.Arguments) { + message := make([]byte, len(data)) + handshakeData.encryptor.XORKeyStream(message, data) + copy(args.Get(0).([]byte), message) // nolint: forcetypeassert + }) + + n, err := handshakeData.proxyConn.Read(buffer) + + assert.EqualValues(t, len(data), n) + assert.NoError(t, err) + assert.Equal(t, data, buffer) + handshakeData.connMock.AssertExpectations(t) + }) +} diff --git a/mtglib/internal/obfuscated2/server_handshake_test.go b/mtglib/internal/obfuscated2/server_handshake_test.go index 5321c9a..09943ae 100644 --- a/mtglib/internal/obfuscated2/server_handshake_test.go +++ b/mtglib/internal/obfuscated2/server_handshake_test.go @@ -1,14 +1,8 @@ package obfuscated2_test import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "encoding/base64" "testing" - "github.com/9seconds/mtg/v2/internal/testlib" - "github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -16,64 +10,31 @@ import ( type ServerHandshakeTestSuite struct { suite.Suite - connMock *testlib.EssentialsConnMock - proxyConn obfuscated2.Conn - encryptor cipher.Stream - decryptor cipher.Stream + data ServerHandshakeTestData } func (suite *ServerHandshakeTestSuite) SetupTest() { - buf := &bytes.Buffer{} - suite.connMock = &testlib.EssentialsConnMock{} - - encryptor, decryptor, err := obfuscated2.ServerHandshake(buf) - suite.NoError(err) - - suite.proxyConn = obfuscated2.Conn{ - Conn: suite.connMock, - Encryptor: encryptor, - Decryptor: decryptor, - } - - serverEncrypted := buf.Bytes() - - decBlock, _ := aes.NewCipher(serverEncrypted[8 : 8+32]) - suite.decryptor = cipher.NewCTR(decBlock, serverEncrypted[8+32:8+32+16]) - - serverDecrypted := make([]byte, len(serverEncrypted)) - suite.decryptor.XORKeyStream(serverDecrypted, serverEncrypted) - - suite.Equal("3d3d3Q", - base64.RawStdEncoding.EncodeToString(serverDecrypted[8+32+16:8+32+16+4])) - - serverEncryptedReverted := make([]byte, len(serverEncrypted)) - - for i := 0; i < 32+16; i++ { - serverEncryptedReverted[8+i] = serverEncrypted[8+32+16-1-i] - } - - encBlock, _ := aes.NewCipher(serverEncryptedReverted[8 : 8+32]) - suite.encryptor = cipher.NewCTR(encBlock, serverEncryptedReverted[8+32:8+32+16]) + suite.data = NewServerHandshakeTestData(suite.T()) } func (suite *ServerHandshakeTestSuite) TearDownTest() { - suite.connMock.AssertExpectations(suite.T()) + suite.data.connMock.AssertExpectations(suite.T()) } func (suite *ServerHandshakeTestSuite) TestSendToTelegram() { messageToTelegram := []byte{10, 11, 12, 13, 14, 'a'} - suite.connMock. + suite.data.connMock. On("Write", mock.Anything). Return(len(messageToTelegram), nil). Once(). Run(func(args mock.Arguments) { message := make([]byte, len(messageToTelegram)) - suite.decryptor.XORKeyStream(message, args.Get(0).([]byte)) // nolint: forcetypeassert + suite.data.decryptor.XORKeyStream(message, args.Get(0).([]byte)) // nolint: forcetypeassert suite.Equal(messageToTelegram, message) }) - n, err := suite.proxyConn.Write(messageToTelegram) + n, err := suite.data.proxyConn.Write(messageToTelegram) suite.EqualValues(len(messageToTelegram), n) suite.NoError(err) } @@ -82,17 +43,17 @@ func (suite *ServerHandshakeTestSuite) TestRecieveFromTelegram() { messageFromTelegram := []byte{10, 11, 12, 13, 14, 'a'} buffer := make([]byte, len(messageFromTelegram)) - suite.connMock. + suite.data.connMock. On("Read", mock.Anything). Return(len(messageFromTelegram), nil). Once(). Run(func(args mock.Arguments) { message := make([]byte, len(messageFromTelegram)) - suite.encryptor.XORKeyStream(message, messageFromTelegram) + suite.data.encryptor.XORKeyStream(message, messageFromTelegram) copy(args.Get(0).([]byte), message) // nolint: forcetypeassert }) - n, err := suite.proxyConn.Read(buffer) + n, err := suite.data.proxyConn.Read(buffer) suite.EqualValues(len(messageFromTelegram), n) suite.NoError(err) suite.Equal(messageFromTelegram, buffer) diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 86e882f..e8d3b44 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -34,7 +34,7 @@ type Proxy struct { network Network antiReplayCache AntiReplayCache blocklist IPBlocklist - whitelist IPBlocklist + allowlist IPBlocklist eventStream EventStream logger Logger } @@ -91,7 +91,7 @@ func (p *Proxy) ServeConn(conn essentials.Conn) { } // Serve starts a proxy on a given listener. -func (p *Proxy) Serve(listener net.Listener) error { // nolint: cyclop +func (p *Proxy) Serve(listener net.Listener) error { p.streamWaitGroup.Add(1) defer p.streamWaitGroup.Done() @@ -109,10 +109,10 @@ func (p *Proxy) Serve(listener net.Listener) error { // nolint: cyclop ipAddr := conn.RemoteAddr().(*net.TCPAddr).IP // nolint: forcetypeassert logger := p.logger.BindStr("ip", ipAddr.String()) - if p.whitelist != nil && !p.whitelist.Contains(ipAddr) { + if !p.allowlist.Contains(ipAddr) { conn.Close() - logger.Info("ip was rejected by whitelist") - p.eventStream.Send(p.ctx, NewEventIPBlocklisted(ipAddr)) + logger.Info("ip was rejected by allowlist") + p.eventStream.Send(p.ctx, NewEventIPAllowlisted(ipAddr)) continue } @@ -145,10 +145,7 @@ func (p *Proxy) Shutdown() { p.streamWaitGroup.Wait() p.workerPool.Release() - if p.whitelist != nil { - p.whitelist.Shutdown() - } - + p.allowlist.Shutdown() p.blocklist.Shutdown() } @@ -308,7 +305,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { network: opts.Network, antiReplayCache: opts.AntiReplayCache, blocklist: opts.IPBlocklist, - whitelist: opts.IPWhitelist, + allowlist: opts.IPAllowlist, eventStream: opts.EventStream, logger: opts.getLogger("proxy"), domainFrontingPort: opts.getDomainFrontingPort(), diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index faad210..c10463c 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -28,10 +28,10 @@ type ProxyOpts struct { // This is a mandatory setting. IPBlocklist IPBlocklist - // IPWhitelist defines a whitelist of IPs to allow to use proxy. + // IPAllowlist defines a whitelist of IPs to allow to use proxy. // // This is an optional setting, ignored by default (no restrictions). - IPWhitelist IPBlocklist + IPAllowlist IPBlocklist // EventStream defines an instance of event stream. // @@ -125,6 +125,8 @@ func (p ProxyOpts) valid() error { return ErrAntiReplayCacheIsNotDefined case p.IPBlocklist == nil: return ErrIPBlocklistIsNotDefined + case p.IPAllowlist == nil: + return ErrIPAllowlistIsNotDefined case p.EventStream == nil: return ErrEventStreamIsNotDefined case p.Logger == nil: diff --git a/mtglib/proxy_test.go b/mtglib/proxy_test.go index 59963c6..480b0b3 100644 --- a/mtglib/proxy_test.go +++ b/mtglib/proxy_test.go @@ -15,6 +15,7 @@ import ( "github.com/9seconds/mtg/v2/antireplay" "github.com/9seconds/mtg/v2/events" "github.com/9seconds/mtg/v2/ipblocklist" + "github.com/9seconds/mtg/v2/ipblocklist/files" "github.com/9seconds/mtg/v2/logger" "github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/network" @@ -22,6 +23,7 @@ import ( "github.com/gotd/td/telegram/dcs" "github.com/gotd/td/tg" "github.com/stretchr/testify/suite" + "github.com/yl2chen/cidranger" ) type ProxyTestSuite struct { @@ -49,11 +51,26 @@ func (suite *ProxyTestSuite) SetupSuite() { ntw, err := network.NewNetwork(dialer, "mtgtest", "1.1.1.1", 0) suite.NoError(err) + allowlist, _ := ipblocklist.NewFireholFromFiles( + logger.NewNoopLogger(), + 1, + []files.File{ + files.NewMem([]*net.IPNet{ + cidranger.AllIPv4, + cidranger.AllIPv6, + }), + }, + nil, + ) + + go allowlist.Run(time.Second) + suite.opts = &mtglib.ProxyOpts{ Secret: mtglib.GenerateSecret("httpbin.org"), Network: ntw, AntiReplayCache: antireplay.NewNoop(), IPBlocklist: ipblocklist.NewNoop(), + IPAllowlist: allowlist, EventStream: events.NewNoopStream(), Logger: logger.NewNoopLogger(), UseTestDCs: true, @@ -114,6 +131,14 @@ func (suite *ProxyTestSuite) TestCannotInitNoIPBlocklist() { suite.Error(err) } +func (suite *ProxyTestSuite) TestCannotInitNoIPAllowlist() { + opts := *suite.opts + opts.IPAllowlist = nil + + _, err := mtglib.NewProxy(opts) + suite.Error(err) +} + func (suite *ProxyTestSuite) TestCannotInitNoEventStream() { opts := *suite.opts opts.EventStream = nil diff --git a/network/circuit_breaker.go b/network/circuit_breaker.go index 91e1317..e5f2702 100644 --- a/network/circuit_breaker.go +++ b/network/circuit_breaker.go @@ -36,7 +36,8 @@ func (c *circuitBreakerDialer) Dial(network, address string) (essentials.Conn, e } func (c *circuitBreakerDialer) DialContext(ctx context.Context, - network, address string) (essentials.Conn, error) { + network, address string, +) (essentials.Conn, error) { switch atomic.LoadUint32(&c.state) { case circuitBreakerStateClosed: return c.doClosed(ctx, network, address) @@ -48,7 +49,8 @@ func (c *circuitBreakerDialer) DialContext(ctx context.Context, } func (c *circuitBreakerDialer) doClosed(ctx context.Context, - network, address string) (essentials.Conn, error) { + network, address string, +) (essentials.Conn, error) { conn, err := c.Dialer.DialContext(ctx, network, address) select { @@ -80,7 +82,8 @@ func (c *circuitBreakerDialer) doClosed(ctx context.Context, } func (c *circuitBreakerDialer) doHalfOpened(ctx context.Context, - network, address string) (essentials.Conn, error) { + network, address string, +) (essentials.Conn, error) { if !atomic.CompareAndSwapUint32(&c.halfOpenAttempts, 0, 1) { return nil, ErrCircuitBreakerOpened } @@ -174,14 +177,16 @@ func (c *circuitBreakerDialer) stopTimer(timerRef **time.Timer) { } func (c *circuitBreakerDialer) ensureTimer(timerRef **time.Timer, - timeout time.Duration, callback func()) { + timeout time.Duration, callback func(), +) { if *timerRef == nil { *timerRef = time.AfterFunc(timeout, callback) } } func newCircuitBreakerDialer(baseDialer Dialer, - openThreshold uint32, halfOpenTimeout, resetFailuresTimeout time.Duration) Dialer { + openThreshold uint32, halfOpenTimeout, resetFailuresTimeout time.Duration, +) Dialer { cb := &circuitBreakerDialer{ Dialer: baseDialer, stateMutexChan: make(chan bool, 1), diff --git a/network/network.go b/network/network.go index e42508f..6aecd6a 100644 --- a/network/network.go +++ b/network/network.go @@ -61,7 +61,8 @@ func (n *network) DialContext(ctx context.Context, protocol, address string) (es } func (n *network) MakeHTTPClient(dialFunc func(ctx context.Context, - network, address string) (essentials.Conn, error)) *http.Client { + network, address string) (essentials.Conn, error), +) *http.Client { if dialFunc == nil { dialFunc = n.DialContext } @@ -123,7 +124,8 @@ func (n *network) dnsResolve(protocol, address string) ([]string, error) { // It brings simple DNS cache and DNS-Over-HTTPS when necessary. func NewNetwork(dialer Dialer, userAgent, dohHostname string, - httpTimeout time.Duration) (mtglib.Network, error) { + httpTimeout time.Duration, +) (mtglib.Network, error) { switch { case httpTimeout < 0: return nil, fmt.Errorf("timeout should be positive number %s", httpTimeout) @@ -146,7 +148,8 @@ func NewNetwork(dialer Dialer, func makeHTTPClient(userAgent string, timeout time.Duration, - dialFunc func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client { + dialFunc func(ctx context.Context, network, address string) (essentials.Conn, error), +) *http.Client { return &http.Client{ Timeout: timeout, Transport: networkHTTPTransport{ diff --git a/stats/prometheus.go b/stats/prometheus.go index 17882df..eec64b5 100644 --- a/stats/prometheus.go +++ b/stats/prometheus.go @@ -110,8 +110,13 @@ func (p prometheusProcessor) EventConcurrencyLimited(_ mtglib.EventConcurrencyLi p.factory.metricConcurrencyLimited.Inc() } -func (p prometheusProcessor) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) { - p.factory.metricIPBlocklisted.Inc() +func (p prometheusProcessor) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) { + tag := TagIPListBlock + if !evt.IsBlockList { + tag = TagIPListAllow + } + + p.factory.metricIPBlocklisted.WithLabelValues(tag).Inc() } func (p prometheusProcessor) EventReplayAttack(_ mtglib.EventReplayAttack) { @@ -150,10 +155,10 @@ type PrometheusFactory struct { metricTelegramTraffic *prometheus.CounterVec metricDomainFrontingTraffic *prometheus.CounterVec + metricIPBlocklisted *prometheus.CounterVec metricDomainFronting prometheus.Counter metricConcurrencyLimited prometheus.Counter - metricIPBlocklisted prometheus.Counter metricReplayAttacks prometheus.Counter } @@ -223,6 +228,11 @@ func NewPrometheus(metricPrefix, httpPath string) *PrometheusFactory { // nolint Name: MetricDomainFrontingTraffic, Help: "Traffic which is generated talking with front domain.", }, []string{TagDirection}), + metricIPBlocklisted: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: metricPrefix, + Name: MetricIPBlocklisted, + Help: "A number of rejected sessions due to ip blocklisting.", + }, []string{TagIPList}), metricDomainFronting: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: metricPrefix, @@ -234,11 +244,6 @@ func NewPrometheus(metricPrefix, httpPath string) *PrometheusFactory { // nolint Name: MetricConcurrencyLimited, Help: "A number of sessions that were rejected by concurrency limiter.", }), - metricIPBlocklisted: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: metricPrefix, - Name: MetricIPBlocklisted, - Help: "A number of rejected sessions due to ip blocklisting.", - }), metricReplayAttacks: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: metricPrefix, Name: MetricReplayAttacks, @@ -253,10 +258,10 @@ func NewPrometheus(metricPrefix, httpPath string) *PrometheusFactory { // nolint registry.MustRegister(factory.metricTelegramTraffic) registry.MustRegister(factory.metricDomainFrontingTraffic) + registry.MustRegister(factory.metricIPBlocklisted) registry.MustRegister(factory.metricDomainFronting) registry.MustRegister(factory.metricConcurrencyLimited) - registry.MustRegister(factory.metricIPBlocklisted) registry.MustRegister(factory.metricReplayAttacks) return factory diff --git a/stats/prometheus_test.go b/stats/prometheus_test.go index e530095..9adcb1b 100644 --- a/stats/prometheus_test.go +++ b/stats/prometheus_test.go @@ -156,7 +156,18 @@ func (suite *PrometheusTestSuite) TestEventIPBlocklisted() { data, err := suite.Get() suite.NoError(err) - suite.Contains(data, `mtg_ip_blocklisted 1`) + suite.Contains(data, `mtg_ip_blocklisted{ip_list="blocklist"} 1`) +} + +func (suite *PrometheusTestSuite) TestEventIPAllowlisted() { + suite.prometheus.EventIPBlocklisted( + mtglib.NewEventIPAllowlisted(net.ParseIP("2001:db8::68"))) + + time.Sleep(100 * time.Millisecond) + + data, err := suite.Get() + suite.NoError(err) + suite.Contains(data, `mtg_ip_blocklisted{ip_list="allowlist"} 1`) } func (suite *PrometheusTestSuite) TestEventReplayAttack() { diff --git a/stats/statsd.go b/stats/statsd.go index bd3f6fb..1c6a99f 100644 --- a/stats/statsd.go +++ b/stats/statsd.go @@ -113,8 +113,13 @@ func (s statsdProcessor) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimite s.client.Incr(MetricConcurrencyLimited, 1) } -func (s statsdProcessor) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) { - s.client.Incr(MetricIPBlocklisted, 1) +func (s statsdProcessor) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) { + tag := TagIPListBlock + if !evt.IsBlockList { + tag = TagIPListAllow + } + + s.client.Incr(MetricIPBlocklisted, 1, statsd.StringTag(TagIPList, tag)) } func (s statsdProcessor) EventReplayAttack(_ mtglib.EventReplayAttack) { @@ -171,7 +176,8 @@ func (s StatsdFactory) Make() events.Observer { // // Valid tagFormats are 'datadog', 'influxdb' and 'graphite'. func NewStatsd(address string, log logger.StdLikeLogger, - metricPrefix, tagFormat string) (StatsdFactory, error) { + metricPrefix, tagFormat string, +) (StatsdFactory, error) { options := []statsd.Option{ statsd.MetricPrefix(metricPrefix), statsd.Logger(log), diff --git a/stats/statsd_test.go b/stats/statsd_test.go index f3eaafd..6107f3c 100644 --- a/stats/statsd_test.go +++ b/stats/statsd_test.go @@ -186,7 +186,15 @@ func (suite *StatsdTestSuite) TestEventIPBlocklisted() { mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10"))) time.Sleep(statsdSleepTime) - suite.Equal("mtg.ip_blocklisted:1|c", suite.statsdServer.String()) + suite.Equal("mtg.ip_blocklisted:1|c|#ip_list:blocklist", suite.statsdServer.String()) +} + +func (suite *StatsdTestSuite) TestEventIPAllowlisted() { + suite.statsd.EventIPBlocklisted( + mtglib.NewEventIPAllowlisted(net.ParseIP("10.0.0.10"))) + + time.Sleep(statsdSleepTime) + suite.Equal("mtg.ip_blocklisted:1|c|#ip_list:allowlist", suite.statsdServer.String()) } func (suite *StatsdTestSuite) TestEventReplayAttack() {