├── internal ├── utils │ ├── testdata │ │ ├── empty.toml │ │ ├── broken.toml │ │ ├── missed-secret.toml │ │ ├── missed-bindto.toml │ │ └── minimal.toml │ ├── make_qr_code_url.go │ ├── root_context_windows.go │ ├── root_context.go │ ├── read_config.go │ ├── make_qr_code_url_test.go │ ├── net_listener.go │ └── read_config_test.go ├── config │ ├── testdata │ │ ├── broken.toml │ │ ├── only_secret.toml │ │ └── minimal.toml │ ├── type_http_path.go │ ├── type_bool.go │ ├── type_ip.go │ ├── type_metric_prefix.go │ ├── type_port.go │ ├── type_concurrency.go │ ├── type_error_rate.go │ ├── type_duration.go │ ├── type_bytes.go │ ├── type_proxy_url.go │ ├── type_hostport.go │ ├── type_statsd_tag_format.go │ ├── config_test.go │ ├── type_prefer_ip.go │ ├── type_http_path_test.go │ ├── type_port_test.go │ ├── type_concurrency_test.go │ ├── type_blocklist_uri.go │ ├── type_metric_prefix_test.go │ ├── type_error_rate_test.go │ ├── type_bytes_test.go │ ├── type_hostport_test.go │ ├── type_bool_test.go │ ├── type_proxy_url_test.go │ ├── type_ip_test.go │ ├── config.go │ ├── type_duration_test.go │ ├── type_statsd_tag_format_test.go │ ├── type_blocklist_uri_test.go │ └── type_prefer_ip_test.go ├── testlib │ ├── events_observer_mock.go │ ├── mtglib_antireplay_cache_mock.go │ ├── capture_output.go │ ├── mtglib_network_mock.go │ └── net_conn_mock.go └── cli │ ├── cli.go │ ├── run.go │ └── generate_secret.go ├── ipblocklist ├── files │ ├── testdata │ │ ├── directory │ │ │ └── .gitkeep │ │ └── readable │ ├── doc.go │ ├── init.go │ ├── local.go │ ├── mem.go │ ├── mem_test.go │ ├── local_test.go │ ├── http.go │ └── http_test.go ├── testdata │ ├── remote_ipset.ipset │ ├── broken_ipset.ipset │ └── good_ipset.ipset ├── noop.go ├── init.go └── noop_test.go ├── .codecov.yml ├── .dockerignore ├── .gitignore ├── mtglib ├── internal │ ├── relay │ │ ├── init_test.go │ │ ├── init.go │ │ ├── pools.go │ │ ├── relay.go │ │ └── relay_test.go │ ├── obfuscated2 │ │ ├── utils.go │ │ ├── testdata │ │ │ ├── client-handshake-snapshot-4529d55776e2d427.json │ │ │ └── client-handshake-snapshot-585c944d672f60a2.json │ │ ├── conn.go │ │ ├── pools.go │ │ ├── client_handshake_fuzz_internal_test.go │ │ ├── server_handshake_fuzz_internal_test.go │ │ ├── client_handshake.go │ │ ├── server_handshake_fuzz_test.go │ │ ├── server_handshake.go │ │ ├── server_handshake_test.go │ │ ├── handshake_frame_internal_test.go │ │ ├── handshake_frame.go │ │ └── client_handshake_test.go │ ├── faketls │ │ ├── record │ │ │ ├── pools.go │ │ │ ├── testdata │ │ │ │ ├── 05eb6b71f87b6802.json │ │ │ │ ├── 4eef4abc15b206b6.json │ │ │ │ ├── 736f358216afe91f.json │ │ │ │ ├── 8405d94222bd0b6a.json │ │ │ │ ├── 9036f76e517f0cd1.json │ │ │ │ ├── 9244766a0fe4a02a.json │ │ │ │ ├── 9255c73d3de76e7b.json │ │ │ │ ├── aeb65b9924315cf8.json │ │ │ │ ├── b0acd44296056b54.json │ │ │ │ ├── c0545a13fd9a3fa3.json │ │ │ │ ├── f083f4501668b759.json │ │ │ │ └── f5696bcdffd11706.json │ │ │ ├── init.go │ │ │ ├── record.go │ │ │ ├── init_test.go │ │ │ └── record_test.go │ │ ├── pools.go │ │ ├── client_hello_fuzz_test.go │ │ ├── testdata │ │ │ ├── client-hello-bad-fa2e46cdb33e2a1b.json │ │ │ ├── client-hello-ok-19dfe38384b9884b.json │ │ │ ├── client-hello-ok-48f8a72a56f3174a.json │ │ │ ├── client-hello-ok-651054256093c6cd.json │ │ │ ├── client-hello-ok-79d01ef18a9d2621.json │ │ │ └── client-hello-ok-7a5569f05b118145.json │ │ ├── conn.go │ │ ├── init.go │ │ ├── welcome_test.go │ │ └── welcome.go │ └── telegram │ │ ├── address_pool.go │ │ ├── telegram.go │ │ └── init.go ├── init_internal_test.go ├── conns.go ├── stream_context.go ├── stream_context_internal_test.go ├── events_test.go └── secret_test.go ├── network ├── sockopts_windows.go ├── init_internal_test.go ├── sockopts_unix.go ├── proxy_dialer.go ├── dns_resolver_internal_test.go ├── sockopts.go ├── socks5_test.go ├── default.go ├── default_test.go ├── load_balanced_socks5.go ├── dns_resolver.go ├── network_test.go ├── init_test.go ├── load_balanced_socks5_test.go └── proxy_dialer_internal_test.go ├── essentials ├── doc.go └── conns.go ├── antireplay ├── noop.go ├── noop_test.go ├── init.go ├── stable_bloom_filter_test.go └── stable_bloom_filter.go ├── .golangci.toml ├── stats ├── pools.go └── stream_info.go ├── logger ├── init.go ├── noop.go ├── noop_test.go └── zerolog.go ├── SECURITY.md ├── Dockerfile ├── main.go ├── LICENSE ├── events ├── init_test.go ├── noop.go ├── noop_test.go ├── init.go └── event_stream.go ├── .goreleaser.yml ├── go.mod └── .github └── workflows └── codeql-analysis.yml /internal/utils/testdata/empty.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/utils/testdata/broken.toml: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /ipblocklist/files/testdata/directory/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipblocklist/files/testdata/readable: -------------------------------------------------------------------------------- 1 | Hooray! 2 | -------------------------------------------------------------------------------- /internal/config/testdata/broken.toml: -------------------------------------------------------------------------------- 1 | s = sdfsdfds 2 | -------------------------------------------------------------------------------- /ipblocklist/testdata/remote_ipset.ipset: -------------------------------------------------------------------------------- 1 | 10.2.2.2 2 | -------------------------------------------------------------------------------- /internal/utils/testdata/missed-secret.toml: -------------------------------------------------------------------------------- 1 | bind-to = "0.0.0.0:80" 2 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | fixes: 4 | - "github.com/9seconds/mtg/v2/::" 5 | -------------------------------------------------------------------------------- /internal/config/testdata/only_secret.toml: -------------------------------------------------------------------------------- 1 | secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" 2 | -------------------------------------------------------------------------------- /internal/utils/testdata/missed-bindto.toml: -------------------------------------------------------------------------------- 1 | 2 | secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t" 3 | -------------------------------------------------------------------------------- /internal/config/testdata/minimal.toml: -------------------------------------------------------------------------------- 1 | secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" 2 | bind-to = "0.0.0.0:3128" 3 | -------------------------------------------------------------------------------- /internal/utils/testdata/minimal.toml: -------------------------------------------------------------------------------- 1 | secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t" 2 | bind-to = "0.0.0.0:80" 3 | -------------------------------------------------------------------------------- /ipblocklist/testdata/broken_ipset.ipset: -------------------------------------------------------------------------------- 1 | # 2 | # This is an intentionally broken ipset. 3 | # 4 | 5 | ajsdkfbd 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | tags 3 | .github/ 4 | .bin/ 5 | *.md 6 | mtg 7 | .golangci.toml 8 | .gitignore 9 | run.sh 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | mtg 9 | vendor/ 10 | ccbuilds/ 11 | .bin/ 12 | coverage.txt 13 | dist/ 14 | -------------------------------------------------------------------------------- /mtglib/internal/relay/init_test.go: -------------------------------------------------------------------------------- 1 | package relay_test 2 | 3 | type loggerMock struct{} 4 | 5 | func (l loggerMock) Printf(format string, args ...interface{}) {} 6 | -------------------------------------------------------------------------------- /internal/testlib/events_observer_mock.go: -------------------------------------------------------------------------------- 1 | package testlib 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | type EventsObserverMock struct { 6 | mock.Mock 7 | } 8 | -------------------------------------------------------------------------------- /ipblocklist/testdata/good_ipset.ipset: -------------------------------------------------------------------------------- 1 | # 2 | # This is very good ipset 3 | # 4 | 5 | 10.0.0.10 # just an example 6 | 10.1.0.0/24 7 | 2001:0db8:85a3:0000:0000:8a2e:0370:7334 8 | -------------------------------------------------------------------------------- /mtglib/internal/relay/init.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | const ( 4 | copyBufferSize = 64 * 1024 5 | ) 6 | 7 | type Logger interface { 8 | Printf(msg string, args ...interface{}) 9 | } 10 | -------------------------------------------------------------------------------- /network/sockopts_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package network 5 | 6 | import "syscall" 7 | 8 | func setSocketReuseAddrPort(conn syscall.RawConn) error { 9 | return nil 10 | } 11 | -------------------------------------------------------------------------------- /internal/testlib/mtglib_antireplay_cache_mock.go: -------------------------------------------------------------------------------- 1 | package testlib 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | type MtglibAntiReplayCacheMock struct { 6 | mock.Mock 7 | } 8 | 9 | func (m *MtglibAntiReplayCacheMock) SeenBefore(data []byte) bool { 10 | return m.Called(data).Bool(0) 11 | } 12 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/utils.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | ) 7 | 8 | func makeAesCtr(key, iv []byte) cipher.Stream { 9 | block, err := aes.NewCipher(key) 10 | if err != nil { 11 | panic(err) 12 | } 13 | 14 | return cipher.NewCTR(block, iv) 15 | } 16 | -------------------------------------------------------------------------------- /essentials/doc.go: -------------------------------------------------------------------------------- 1 | // This is a minimal package that contains _essentials_ of mtglib and its 2 | // complimentary packages. This is mostly required to comply some interfaces 3 | // between mtglib and its internals to avoid circular dependencies. 4 | // 5 | // This package should contain only bare minimum and mostly technical. 6 | package essentials 7 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/pools.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var recordPool = sync.Pool{ 8 | New: func() interface{} { 9 | return &Record{} 10 | }, 11 | } 12 | 13 | func AcquireRecord() *Record { 14 | return recordPool.Get().(*Record) //nolint: forcetypeassert 15 | } 16 | 17 | func ReleaseRecord(r *Record) { 18 | r.Reset() 19 | recordPool.Put(r) 20 | } 21 | -------------------------------------------------------------------------------- /antireplay/noop.go: -------------------------------------------------------------------------------- 1 | package antireplay 2 | 3 | import "github.com/9seconds/mtg/v2/mtglib" 4 | 5 | type noop struct{} 6 | 7 | func (n noop) SeenBefore(_ []byte) bool { return false } 8 | 9 | // NewNoop returns an implementation that does nothing. A corresponding method 10 | // always returns false, so this cache accepts everything you pass to it. 11 | func NewNoop() mtglib.AntiReplayCache { 12 | return noop{} 13 | } 14 | -------------------------------------------------------------------------------- /ipblocklist/files/doc.go: -------------------------------------------------------------------------------- 1 | // files defines a set of abstraction for 'files': an openable entities that 2 | // could be read after. 3 | // 4 | // This is not a file on a filesystem of your local machine, it also can 5 | // include "in memory" files or even remote ones, like HTTP endpoints. If you 6 | // make a GET request to HTTP endpoint, then a body is readable and you can 7 | // consider it as an openable file. 8 | package files 9 | -------------------------------------------------------------------------------- /mtglib/internal/relay/pools.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import "sync" 4 | 5 | var copyBufferPool = sync.Pool{ 6 | New: func() interface{} { 7 | rv := make([]byte, copyBufferSize) 8 | 9 | return &rv 10 | }, 11 | } 12 | 13 | func acquireCopyBuffer() *[]byte { 14 | return copyBufferPool.Get().(*[]byte) //nolint: forcetypeassert 15 | } 16 | 17 | func releaseCopyBuffer(buf *[]byte) { 18 | copyBufferPool.Put(buf) 19 | } 20 | -------------------------------------------------------------------------------- /.golangci.toml: -------------------------------------------------------------------------------- 1 | [run] 2 | concurrency = 4 3 | deadline = "2m" 4 | tests = true 5 | skip-dirs = ["vendor"] 6 | 7 | [output] 8 | format = "colored-line-number" 9 | 10 | [linters] 11 | enable-all = true 12 | disable = [ 13 | "containedctx", 14 | "exhaustivestruct", 15 | "exhaustruct", 16 | "gas", 17 | "gochecknoglobals", 18 | "goerr113", 19 | "ireturn", 20 | "thelper", 21 | "varnamelen", 22 | ] 23 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-4529d55776e2d427.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret": "NnoYmu4Y+jHBkAVO/UqOlQ", 3 | "frame": "gDcXwaMY4RwlR+nJw+ILDr123UJHHjjE/U5pF4m/Y04AmH7lEpEL6UYRnIYDbDlOHSDxc1ToziPvNlJJh8RMow", 4 | "dc": 2, 5 | "encrypted": { 6 | "text": "AQIDBAUGBwgJCg", 7 | "cipher": "wZV3TR39l9nRoQ" 8 | }, 9 | "decrypted": { 10 | "text": "4wZj6mUUew", 11 | "cipher": "YWJjZGVmZw" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-585c944d672f60a2.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret": "NnoYmu4Y+jHBkAVO/UqOlQ", 3 | "frame": "M2WyxeiwIQB+ZOFxNzSNHtu9OdESkfxv3JkKFimCxUoYA3BD/Ql9nXB/OIonCKLUKCcS0VzZ2P6/+5oQ9GI8YA", 4 | "dc": 2, 5 | "encrypted": { 6 | "text": "AQIDBAUGBwgJCg", 7 | "cipher": "tzAwrCz00odERg" 8 | }, 9 | "decrypted": { 10 | "text": "QkIvwGQDgA", 11 | "cipher": "YWJjZGVmZw" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/utils/make_qr_code_url.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net/url" 4 | 5 | func MakeQRCodeURL(data string) string { 6 | values := url.Values{} 7 | values.Set("qzone", "4") 8 | values.Set("format", "svg") 9 | values.Set("data", data) 10 | 11 | rv := url.URL{ 12 | Scheme: "https", 13 | Host: "api.qrserver.com", 14 | Path: "/v1/create-qr-code", 15 | RawQuery: values.Encode(), 16 | } 17 | 18 | return rv.String() 19 | } 20 | -------------------------------------------------------------------------------- /stats/pools.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "sync" 4 | 5 | var streamInfoPool = sync.Pool{ 6 | New: func() interface{} { 7 | return &streamInfo{ 8 | tags: make(map[string]string), 9 | } 10 | }, 11 | } 12 | 13 | func acquireStreamInfo() *streamInfo { 14 | return streamInfoPool.Get().(*streamInfo) //nolint: forcetypeassert 15 | } 16 | 17 | func releaseStreamInfo(info *streamInfo) { 18 | info.Reset() 19 | streamInfoPool.Put(info) 20 | } 21 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/pools.go: -------------------------------------------------------------------------------- 1 | package faketls 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | var bytesBufferPool = sync.Pool{ 9 | New: func() interface{} { 10 | return &bytes.Buffer{} 11 | }, 12 | } 13 | 14 | func acquireBytesBuffer() *bytes.Buffer { 15 | return bytesBufferPool.Get().(*bytes.Buffer) //nolint: forcetypeassert 16 | } 17 | 18 | func releaseBytesBuffer(b *bytes.Buffer) { 19 | b.Reset() 20 | bytesBufferPool.Put(b) 21 | } 22 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/05eb6b71f87b6802.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 20, 3 | "version": 772, 4 | "payload": "sxS+0oAyk+NBv0LLVtQOp9WSx4CweyUZPz01tQ0o4oyp8aaBl6/kMFvLq3q52KE8lCiKejLw2NxVBUkE+4izCf2gLx9qfr81opWnqJTChWzcDijvttbq9cmtDFNL+odKsS3v1/TfYEFtPsoRPrJRmOHRAnqnf49Y5Q==", 5 | "record": "FAMEAHmzFL7SgDKT40G/QstW1A6n1ZLHgLB7JRk/PTW1DSjijKnxpoGXr+QwW8urernYoTyUKIp6MvDY3FUFSQT7iLMJ/aAvH2p+vzWilaeolMKFbNwOKO+21ur1ya0MU0v6h0qxLe/X9N9gQW0+yhE+slGY4dECeqd/j1jl" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/4eef4abc15b206b6.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 22, 3 | "version": 772, 4 | "payload": "waNH223htyxCBKAb6hm0u/SK/9mhI8Ck91nfWob7QMOaIREogrDYREJH4Djcp47XrpAlEaUIDiCvoFLVJ/LK1nYs4swzfHSSl/+Aj1eqPA63XqPa8EG4FAbf0DwjwXxV9qVIhvP9b2TafKbzr4Yb5GCygzFRb/zawA==", 5 | "record": "FgMEAHnBo0fbbeG3LEIEoBvqGbS79Ir/2aEjwKT3Wd9ahvtAw5ohESiCsNhEQkfgONynjteukCURpQgOIK+gUtUn8srWdizizDN8dJKX/4CPV6o8Drdeo9rwQbgUBt/QPCPBfFX2pUiG8/1vZNp8pvOvhhvkYLKDMVFv/NrA" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/736f358216afe91f.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 23, 3 | "version": 769, 4 | "payload": "jmJ0o1E5+ehAHHYAbCo4AMV03X7RSivYl250s06nD9CO44fyjaoGELz0N7IeCg1jFKcRVSCRmYYmiIY9wydn2fXOJhKif8B0BlM3qhbethYgyP+l1S8hyyETpIiOtiiiOnAJwl1D1j9OryFiJFSdRRXReIMZ4CPqPg==", 5 | "record": "FwMBAHmOYnSjUTn56EAcdgBsKjgAxXTdftFKK9iXbnSzTqcP0I7jh/KNqgYQvPQ3sh4KDWMUpxFVIJGZhiaIhj3DJ2fZ9c4mEqJ/wHQGUzeqFt62FiDI/6XVLyHLIROkiI62KKI6cAnCXUPWP06vIWIkVJ1FFdF4gxngI+o+" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/8405d94222bd0b6a.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 22, 3 | "version": 769, 4 | "payload": "hBnpBnNUdlqe/rKXa7Judcz79u7AkUgSGOycn8EqvbkZpVxnI31rNOvAsPZqG+GF7DWJ3R7H2ETmFmrpnyyng32MjSs1jptmV1oAs63zTADD7sVipgid9AJHwfl4CrC3FIQr43IPMYd29JPOl5bqu/SfrgI16PBiJw==", 5 | "record": "FgMBAHmEGekGc1R2Wp7+spdrsm51zPv27sCRSBIY7JyfwSq9uRmlXGcjfWs068Cw9mob4YXsNYndHsfYROYWaumfLKeDfYyNKzWOm2ZXWgCzrfNMAMPuxWKmCJ30AkfB+XgKsLcUhCvjcg8xh3b0k86Xluq79J+uAjXo8GIn" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/9036f76e517f0cd1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 23, 3 | "version": 770, 4 | "payload": "Vm/C+DO56czlbtR915aHzsugSyDtp8CtojF9w1jKY0efyyfcLrNuhNg/pZm3gQ7v2BBbL1UJ97v/RIjST+5gRIfg3bBN1BE9hkf+N2AYY2lHLi0yeInHB0zFWPeHscsDopDFadIi5KtC8HvbEMuK+kK8POVk5tN9UQ==", 5 | "record": "FwMCAHlWb8L4M7npzOVu1H3XlofOy6BLIO2nwK2iMX3DWMpjR5/LJ9wus26E2D+lmbeBDu/YEFsvVQn3u/9EiNJP7mBEh+DdsE3UET2GR/43YBhjaUcuLTJ4iccHTMVY94exywOikMVp0iLkq0Lwe9sQy4r6Qrw85WTm031R" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/9244766a0fe4a02a.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 22, 3 | "version": 770, 4 | "payload": "ajPzpsgk4gwm2stRQKbllvKRLdI7vmyaj1uxEJ/kKoQnQSPumdDNKD618U2Cq6PVd0/b+9YtH67Uzx1QxtpKuby5fUXqw06WUuDAQsmjq7F26EkE5FND6rQUjUPC+e1U0dF4TQzOUSS4IAkFQPAaVehUVTRxVWa/0g==", 5 | "record": "FgMCAHlqM/OmyCTiDCbay1FApuWW8pEt0ju+bJqPW7EQn+QqhCdBI+6Z0M0oPrXxTYKro9V3T9v71i0frtTPHVDG2kq5vLl9RerDTpZS4MBCyaOrsXboSQTkU0PqtBSNQ8L57VTR0XhNDM5RJLggCQVA8BpV6FRVNHFVZr/S" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/9255c73d3de76e7b.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 20, 3 | "version": 771, 4 | "payload": "d1Hiv1NYVgEDR9mtJyv9j8mg3dWqfUpeKfOsL+jzSDfVIxeDiJZFLDT50TjNW44/yEOVEX/Y/pk+wnc7E8aCEiwGwAvB+Insw1UCJ2ejt689VWLo2u4klGVKTHuOpUvdGVTc7Lo4FAt91KQSPLYB5iqxomjEv5e3Vg==", 5 | "record": "FAMDAHl3UeK/U1hWAQNH2a0nK/2PyaDd1ap9Sl4p86wv6PNIN9UjF4OIlkUsNPnROM1bjj/IQ5URf9j+mT7CdzsTxoISLAbAC8H4iezDVQInZ6O3rz1VYuja7iSUZUpMe46lS90ZVNzsujgUC33UpBI8tgHmKrGiaMS/l7dW" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/aeb65b9924315cf8.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 23, 3 | "version": 771, 4 | "payload": "wbdU1CbrzuAJDsh6CFjGyE+AFArJj/Wmsa2wtDyW0kRuE2vUO8gg+nXkg0kkoz0WnvQEOdaswfJIaVrloD78yoyeQVfBB+VUP/63vqn60v5ccaQEn0jLdxgLjiTAxKDQDxCTMRoLnFE2ZZf28zw+HfqpIxiOZs8LhQ==", 5 | "record": "FwMDAHnBt1TUJuvO4AkOyHoIWMbIT4AUCsmP9aaxrbC0PJbSRG4Ta9Q7yCD6deSDSSSjPRae9AQ51qzB8khpWuWgPvzKjJ5BV8EH5VQ//re+qfrS/lxxpASfSMt3GAuOJMDEoNAPEJMxGgucUTZll/bzPD4d+qkjGI5mzwuF" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/b0acd44296056b54.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 23, 3 | "version": 772, 4 | "payload": "qqnBMb1Af3zZt4DPHpVRuIiON9ODGJUNFicFjranORh67L/HI4D6HnHyycZFUSBOw2FjMBF6UialY8snOYaRKrQmQzuUNg1Ztq7yAZ+Lgj3TBarR6OMlYhEAY0Px9Xv1UuJ0YcvQx33gdM1skJ5HBR3yZvEKNJV1LA==", 5 | "record": "FwMEAHmqqcExvUB/fNm3gM8elVG4iI4304MYlQ0WJwWOtqc5GHrsv8cjgPoecfLJxkVRIE7DYWMwEXpSJqVjyyc5hpEqtCZDO5Q2DVm2rvIBn4uCPdMFqtHo4yViEQBjQ/H1e/VS4nRhy9DHfeB0zWyQnkcFHfJm8Qo0lXUs" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/c0545a13fd9a3fa3.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 20, 3 | "version": 769, 4 | "payload": "NEe735TuQFp7bWpFQhASas/e1XaySvus0ovXmkfCbFq334MyFHq2eDMadziXsfu/GfBjoYggvk0LgYUeoAkBNKR0dfSovjSndaqmIUonoWl+6sZObiGZkRIMwuY2q4Eaw4/iuDu/pZhjRW/iAIH+YH7cyk/1tgdJDg==", 5 | "record": "FAMBAHk0R7vflO5AWnttakVCEBJqz97VdrJK+6zSi9eaR8JsWrffgzIUerZ4Mxp3OJex+78Z8GOhiCC+TQuBhR6gCQE0pHR19Ki+NKd1qqYhSiehaX7qxk5uIZmREgzC5jargRrDj+K4O7+lmGNFb+IAgf5gftzKT/W2B0kO" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/f083f4501668b759.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 22, 3 | "version": 771, 4 | "payload": "wrXjZrPm3OSyzO0klv6/G+z2PDloR/colS/RlWwQE31Vb2xm8YkEchDDKwlc/KPLD73qMoz3MQOQLtSLc8LhVYp+l7L9jz49yTaVKtBI5UuGbo09snsKxFCgCyYUBETKabATBQtiaEu/D8dmF4Yk/2ww4sEb8DwKLQ==", 5 | "record": "FgMDAHnCteNms+bc5LLM7SSW/r8b7PY8OWhH9yiVL9GVbBATfVVvbGbxiQRyEMMrCVz8o8sPveoyjPcxA5Au1ItzwuFVin6Xsv2PPj3JNpUq0EjlS4ZujT2yewrEUKALJhQERMppsBMFC2JoS78Px2YXhiT/bDDiwRvwPAot" 6 | } -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/testdata/f5696bcdffd11706.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 20, 3 | "version": 770, 4 | "payload": "OU5s8Sa11hpXWEarWzFlX55IZt3Eo+F4AMbQ/2RwB4rfHS/JNl8n63OR4oYs9QXw3RfCrYJuU9n6Xn+I/+7ZzAgZ0PbLSXW1PrLtttdfmhTErK90b49YEWdY9na4g++NMkKykwgXvY1hNxZIHX/qawEWJgxXUR3DdQ==", 5 | "record": "FAMCAHk5TmzxJrXWGldYRqtbMWVfnkhm3cSj4XgAxtD/ZHAHit8dL8k2Xyfrc5Hihiz1BfDdF8Ktgm5T2fpef4j/7tnMCBnQ9stJdbU+su2211+aFMSsr3Rvj1gRZ1j2driD740yQrKTCBe9jWE3Fkgdf+prARYmDFdRHcN1" 6 | } -------------------------------------------------------------------------------- /internal/utils/root_context_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "os/signal" 10 | ) 11 | 12 | func RootContext() context.Context { 13 | ctx, cancel := context.WithCancel(context.Background()) 14 | sigChan := make(chan os.Signal, 1) 15 | 16 | signal.Notify(sigChan, os.Interrupt) 17 | go func() { 18 | for range sigChan { 19 | cancel() 20 | } 21 | }() 22 | 23 | return ctx 24 | } 25 | -------------------------------------------------------------------------------- /ipblocklist/noop.go: -------------------------------------------------------------------------------- 1 | package ipblocklist 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/9seconds/mtg/v2/mtglib" 8 | ) 9 | 10 | type noop struct{} 11 | 12 | func (n noop) Contains(ip net.IP) bool { return false } 13 | func (n noop) Run(updateEach time.Duration) {} 14 | func (n noop) Shutdown() {} 15 | 16 | // NewNoop returns a dummy ipblocklist which allows all incoming connections. 17 | func NewNoop() mtglib.IPBlocklist { 18 | return noop{} 19 | } 20 | -------------------------------------------------------------------------------- /internal/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "github.com/alecthomas/kong" 4 | 5 | type CLI struct { 6 | GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"` 7 | Access Access `kong:"cmd,help='Print access information.'"` 8 | Run Run `kong:"cmd,help='Run proxy.'"` 9 | SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"` 10 | Version kong.VersionFlag `kong:"help='Print version.',short='v'"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/utils/root_context.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | func RootContext() context.Context { 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | sigChan := make(chan os.Signal, 1) 16 | 17 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 18 | 19 | go func() { 20 | for range sigChan { 21 | cancel() 22 | } 23 | }() 24 | 25 | return ctx 26 | } 27 | -------------------------------------------------------------------------------- /internal/cli/run.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/9seconds/mtg/v2/internal/utils" 7 | ) 8 | 9 | type Run struct { 10 | ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll 11 | } 12 | 13 | func (r *Run) Run(cli *CLI, version string) error { 14 | conf, err := utils.ReadConfig(r.ConfigPath) 15 | if err != nil { 16 | return fmt.Errorf("cannot init config: %w", err) 17 | } 18 | 19 | return runProxy(conf, version) 20 | } 21 | -------------------------------------------------------------------------------- /logger/init.go: -------------------------------------------------------------------------------- 1 | // Package logger has implementation of loggers for [mtglib.Logger] interface. 2 | // 3 | // Please see a description of that interface to get some agreements which are 4 | // used by mtglib. 5 | package logger 6 | 7 | // StdLikeLogger is an interface which is close to [log.Logger]. This is 8 | // commonly used by many 3pp tools. While mtglib itself does not need it, it is 9 | // always a good idea to support it and have a transient end to end logging. 10 | type StdLikeLogger interface { 11 | Printf(format string, args ...interface{}) 12 | } 13 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/client_hello_fuzz_test.go: -------------------------------------------------------------------------------- 1 | package faketls_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/9seconds/mtg/v2/mtglib/internal/faketls" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var FuzzClientHelloSecret = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 11 | 12 | func FuzzClientHello(f *testing.F) { 13 | f.Add([]byte{1, 2, 3}) 14 | 15 | f.Fuzz(func(t *testing.T, frame []byte) { 16 | _, err := faketls.ParseClientHello(FuzzClientHelloSecret, frame) 17 | 18 | // a probability of having != err is almost negligible 19 | require.Error(t, err) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /essentials/conns.go: -------------------------------------------------------------------------------- 1 | package essentials 2 | 3 | import ( 4 | "io" 5 | "net" 6 | ) 7 | 8 | // CloseableReader is an [io.Reader] interface that can close its reading end. 9 | type CloseableReader interface { 10 | io.Reader 11 | CloseRead() error 12 | } 13 | 14 | // CloseableWriter is an [io.Writer] that can close its writing end. 15 | type CloseableWriter interface { 16 | io.Writer 17 | CloseWrite() error 18 | } 19 | 20 | // Conn is an extension of [net.Conn] that can close its ends. This mostly 21 | // implies TCP connections. 22 | type Conn interface { 23 | net.Conn 24 | CloseableReader 25 | CloseableWriter 26 | } 27 | -------------------------------------------------------------------------------- /stats/stream_info.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import statsd "github.com/smira/go-statsd" 4 | 5 | type streamInfo struct { 6 | isDomainFronted bool 7 | tags map[string]string 8 | } 9 | 10 | func (s streamInfo) T(key string) statsd.Tag { 11 | return statsd.StringTag(key, s.tags[key]) 12 | } 13 | 14 | func (s *streamInfo) Reset() { 15 | s.isDomainFronted = false 16 | 17 | for k := range s.tags { 18 | delete(s.tags, k) 19 | } 20 | } 21 | 22 | func getDirection(isRead bool) string { 23 | if isRead { // for telegram 24 | return TagDirectionToClient 25 | } 26 | 27 | return TagDirectionFromClient 28 | } 29 | -------------------------------------------------------------------------------- /internal/utils/read_config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | ) 9 | 10 | func ReadConfig(path string) (*config.Config, error) { 11 | content, err := os.ReadFile(path) 12 | if err != nil { 13 | return nil, fmt.Errorf("cannot read config file: %w", err) 14 | } 15 | 16 | conf, err := config.Parse(content) 17 | if err != nil { 18 | return nil, fmt.Errorf("cannot parse config: %w", err) 19 | } 20 | 21 | if err := conf.Validate(); err != nil { 22 | return nil, fmt.Errorf("invalid config: %w", err) 23 | } 24 | 25 | return conf, nil 26 | } 27 | -------------------------------------------------------------------------------- /ipblocklist/files/init.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | // ErrBadHTTPClient is returned if given HTTP client is initialized 10 | // incorrectly. 11 | var ErrBadHTTPClient = errors.New("incorrect http client") 12 | 13 | // File is an abstraction for a entity that can be opened in some context. 14 | type File interface { 15 | // Open returns an readable entity for a file. It is important to not forget 16 | // to close it after the usage. 17 | Open(context.Context) (io.ReadCloser, error) 18 | 19 | // String returns a short text description for the file 20 | String() string 21 | } 22 | -------------------------------------------------------------------------------- /antireplay/noop_test.go: -------------------------------------------------------------------------------- 1 | package antireplay_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/9seconds/mtg/v2/antireplay" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type NoopTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (suite *NoopTestSuite) TestOp() { 15 | filter := antireplay.NewNoop() 16 | 17 | suite.False(filter.SeenBefore([]byte{1, 2, 3})) 18 | suite.False(filter.SeenBefore([]byte{4, 5, 6})) 19 | suite.False(filter.SeenBefore([]byte{1, 2, 3})) 20 | suite.False(filter.SeenBefore([]byte{4, 5, 6})) 21 | } 22 | 23 | func TestNoop(t *testing.T) { 24 | t.Parallel() 25 | suite.Run(t, &NoopTestSuite{}) 26 | } 27 | -------------------------------------------------------------------------------- /ipblocklist/init.go: -------------------------------------------------------------------------------- 1 | // Package ipblocklist contains default implementation of the 2 | // [mtglib.IPBlocklist] for mtg. 3 | // 4 | // Please check documentation for [mtglib.IPBlocklist] interface to get an idea 5 | // of this abstraction. 6 | package ipblocklist 7 | 8 | import "time" 9 | 10 | const ( 11 | // DefaultFireholDownloadConcurrency defines a default max number of 12 | // concurrent downloads of ip blocklists for Firehol. 13 | DefaultFireholDownloadConcurrency = 1 14 | 15 | // DefaultFireholUpdateEach defines a default time period when Firehol 16 | // requests updates of the blocklists. 17 | DefaultFireholUpdateEach = 6 * time.Hour 18 | ) 19 | -------------------------------------------------------------------------------- /internal/cli/generate_secret.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/9seconds/mtg/v2/mtglib" 7 | ) 8 | 9 | type GenerateSecret struct { 10 | HostName string `kong:"arg,required,help='Hostname to use for domain fronting.',name='hostname'"` 11 | Hex bool `kong:"help='Print secret in hex encoding.',short='x'"` 12 | } 13 | 14 | func (g *GenerateSecret) Run(cli *CLI, _ string) error { 15 | secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName) 16 | 17 | if g.Hex { 18 | fmt.Println(secret.Hex()) //nolint: forbidigo 19 | } else { 20 | fmt.Println(secret.Base64()) //nolint: forbidigo 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /antireplay/init.go: -------------------------------------------------------------------------------- 1 | // Antireplay package has cache implementations that are effective against 2 | // replay attacks. 3 | // 4 | // To understand more about replay attacks, please read documentation for 5 | // [mtglib.AntiReplayCache] interface. This package has a list of some 6 | // implementations of this interface. 7 | package antireplay 8 | 9 | const ( 10 | // DefaultStableBloomFilterMaxSize is a recommended byte size for a stable 11 | // bloom filter. 12 | DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB 13 | 14 | // DefaultStableBloomFilterErrorRate is a recommended default error rate for a 15 | // stable bloom filter. 16 | DefaultStableBloomFilterErrorRate = 0.001 17 | ) 18 | -------------------------------------------------------------------------------- /ipblocklist/noop_test.go: -------------------------------------------------------------------------------- 1 | package ipblocklist_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/ipblocklist" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type NoopTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func (suite *NoopTestSuite) TestOp() { 16 | suite.False(ipblocklist.NewNoop().Contains(net.ParseIP("10.0.0.10"))) 17 | suite.False(ipblocklist.NewNoop().Contains(net.ParseIP("10.0.0.10"))) 18 | } 19 | 20 | func (suite *NoopTestSuite) TestRun() { 21 | blocklist := ipblocklist.NewNoop() 22 | 23 | blocklist.Run(0) 24 | blocklist.Shutdown() 25 | } 26 | 27 | func TestNoop(t *testing.T) { 28 | t.Parallel() 29 | suite.Run(t, &NoopTestSuite{}) 30 | } 31 | -------------------------------------------------------------------------------- /internal/config/type_http_path.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strings" 4 | 5 | type TypeHTTPPath struct { 6 | Value string 7 | } 8 | 9 | func (t *TypeHTTPPath) Set(value string) error { 10 | t.Value = "/" + strings.Trim(value, "/") 11 | 12 | return nil 13 | } 14 | 15 | func (t TypeHTTPPath) Get(defaultValue string) string { 16 | if t.Value == "" { 17 | return defaultValue 18 | } 19 | 20 | return t.Value 21 | } 22 | 23 | func (t *TypeHTTPPath) UnmarshalText(data []byte) error { 24 | return t.Set(string(data)) 25 | } 26 | 27 | func (t TypeHTTPPath) MarshalText() ([]byte, error) { 28 | return []byte(t.String()), nil 29 | } 30 | 31 | func (t TypeHTTPPath) String() string { 32 | return t.Value 33 | } 34 | -------------------------------------------------------------------------------- /network/init_internal_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/9seconds/mtg/v2/essentials" 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | type DialerMock struct { 11 | mock.Mock 12 | } 13 | 14 | func (d *DialerMock) Dial(network, address string) (essentials.Conn, error) { 15 | args := d.Called(network, address) 16 | 17 | return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert 18 | } 19 | 20 | func (d *DialerMock) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) { 21 | args := d.Called(ctx, network, address) 22 | 23 | return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert 24 | } 25 | -------------------------------------------------------------------------------- /antireplay/stable_bloom_filter_test.go: -------------------------------------------------------------------------------- 1 | package antireplay_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/9seconds/mtg/v2/antireplay" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type StableBloomFilterTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (suite *StableBloomFilterTestSuite) TestOp() { 15 | filter := antireplay.NewStableBloomFilter(500, 0.001) 16 | 17 | suite.False(filter.SeenBefore([]byte{1, 2, 3})) 18 | suite.False(filter.SeenBefore([]byte{4, 5, 6})) 19 | suite.True(filter.SeenBefore([]byte{1, 2, 3})) 20 | suite.True(filter.SeenBefore([]byte{4, 5, 6})) 21 | } 22 | 23 | func TestStableBloomFilter(t *testing.T) { 24 | t.Parallel() 25 | suite.Run(t, &StableBloomFilterTestSuite{}) 26 | } 27 | -------------------------------------------------------------------------------- /ipblocklist/files/local.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | type localFile struct { 11 | path string 12 | } 13 | 14 | func (l localFile) Open(ctx context.Context) (io.ReadCloser, error) { 15 | return os.Open(l.path) //nolint: wrapcheck 16 | } 17 | 18 | func (l localFile) String() string { 19 | return l.path 20 | } 21 | 22 | // NewLocal returns an openable File for a path on a local file system. 23 | func NewLocal(path string) (File, error) { 24 | if stat, err := os.Stat(path); os.IsNotExist(err) || stat.IsDir() || stat.Mode().Perm()&0o400 == 0 { 25 | return nil, fmt.Errorf("%s is not a readable file", path) 26 | } 27 | 28 | return localFile{ 29 | path: path, 30 | }, nil 31 | } 32 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We support 2 tracks: 2.x and 1.x 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.x | :white_check_mark: | 10 | | 1.x | :interrobang: | 11 | | < 1.0 | :x: | 12 | 13 | 1.x has adtag support. We do not plan any active development there but we guarantee: 14 | 15 | 1. Regular dependency updates 16 | 2. Updates for Golang versions 17 | 3. Security vulnerability fixes 18 | 4. Merging small PRs 19 | 20 | 2.x is in active development and have a full support. 21 | 22 | ## Reporting a Vulnerability 23 | 24 | If you have found a vulnerability, please report about it to nineseconds@yandex.ru, telegram @9seconds or here in issues 25 | -------------------------------------------------------------------------------- /network/sockopts_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package network 5 | 6 | import ( 7 | "fmt" 8 | "syscall" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func setSocketReuseAddrPort(conn syscall.RawConn) error { 14 | var err error 15 | 16 | conn.Control(func(fd uintptr) { //nolint: errcheck 17 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) //nolint: nosnakecase 18 | if err != nil { 19 | err = fmt.Errorf("cannot set SO_REUSEADDR: %w", err) 20 | 21 | return 22 | } 23 | 24 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) //nolint: nosnakecase 25 | if err != nil { 26 | err = fmt.Errorf("cannot set SO_REUSEPORT: %w", err) 27 | } 28 | }) 29 | 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # BUILD STAGE 3 | 4 | FROM golang:1.19-alpine AS build 5 | 6 | RUN set -x \ 7 | && apk --no-cache --update add \ 8 | bash \ 9 | ca-certificates \ 10 | curl \ 11 | git \ 12 | make 13 | 14 | COPY . /app 15 | WORKDIR /app 16 | 17 | RUN set -x \ 18 | && make -j 4 static 19 | 20 | 21 | ############################################################################### 22 | # PACKAGE STAGE 23 | 24 | FROM scratch 25 | 26 | ENTRYPOINT ["/mtg"] 27 | CMD ["run", "/config.toml"] 28 | 29 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 30 | COPY --from=build /app/mtg /mtg 31 | COPY --from=build /app/example.config.toml /config.toml 32 | -------------------------------------------------------------------------------- /internal/config/type_bool.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type TypeBool struct { 9 | Value bool 10 | } 11 | 12 | func (t *TypeBool) Set(data string) error { 13 | parsed, err := strconv.ParseBool(data) 14 | if err != nil { 15 | return fmt.Errorf("incorrect bool value: %s", data) 16 | } 17 | 18 | t.Value = parsed 19 | 20 | return nil 21 | } 22 | 23 | func (t TypeBool) Get(defaultValue bool) bool { 24 | return t.Value || defaultValue 25 | } 26 | 27 | func (t *TypeBool) UnmarshalJSON(data []byte) error { 28 | return t.Set(string(data)) 29 | } 30 | 31 | func (t TypeBool) MarshalJSON() ([]byte, error) { 32 | return []byte(t.String()), nil 33 | } 34 | 35 | func (t TypeBool) String() string { 36 | return strconv.FormatBool(t.Value) 37 | } 38 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // mtg is just a command-line application that starts a proxy. 2 | // 3 | // Application logic is how to read a config and configure mtglib.Proxy. 4 | // So, probably you need to read the documentation for mtglib package 5 | // first. 6 | // 7 | // mtglib is a core of the application. The rest of the packages provide 8 | // some default implementations for the interfaces, defined in mtglib. 9 | package main 10 | 11 | import ( 12 | "math/rand" 13 | "time" 14 | 15 | "github.com/9seconds/mtg/v2/internal/cli" 16 | "github.com/alecthomas/kong" 17 | ) 18 | 19 | func main() { 20 | rand.Seed(time.Now().UTC().UnixNano()) 21 | 22 | cli := &cli.CLI{} 23 | ctx := kong.Parse(cli, kong.Vars{ 24 | "version": getVersion(), 25 | }) 26 | 27 | ctx.FatalIfErrorf(ctx.Run(cli, version)) 28 | } 29 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/conn.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "crypto/cipher" 5 | 6 | "github.com/9seconds/mtg/v2/essentials" 7 | ) 8 | 9 | type Conn struct { 10 | essentials.Conn 11 | 12 | Encryptor cipher.Stream 13 | Decryptor cipher.Stream 14 | } 15 | 16 | func (c Conn) Read(p []byte) (int, error) { 17 | n, err := c.Conn.Read(p) 18 | if err != nil { 19 | return n, err //nolint: wrapcheck 20 | } 21 | 22 | c.Decryptor.XORKeyStream(p, p[:n]) 23 | 24 | return n, nil 25 | } 26 | 27 | func (c Conn) Write(p []byte) (int, error) { 28 | buf := acquireBytesBuffer() 29 | defer releaseBytesBuffer(buf) 30 | 31 | buf.Write(p) 32 | 33 | payload := buf.Bytes() 34 | c.Encryptor.XORKeyStream(payload, payload) 35 | 36 | return c.Conn.Write(payload) //nolint: wrapcheck 37 | } 38 | -------------------------------------------------------------------------------- /ipblocklist/files/mem.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | type memFile struct { 11 | data string 12 | } 13 | 14 | func (m memFile) Open(ctx context.Context) (io.ReadCloser, error) { 15 | return io.NopCloser(strings.NewReader(m.data)), nil 16 | } 17 | 18 | func (m memFile) String() string { 19 | return "mem" 20 | } 21 | 22 | // NewMem returns an openable file that is kept in RAM. 23 | func NewMem(networks []*net.IPNet) File { 24 | builder := strings.Builder{} 25 | 26 | if len(networks) > 0 { 27 | builder.WriteString(networks[0].String()) 28 | } 29 | 30 | for i := 1; i < len(networks); i++ { 31 | builder.WriteString("\n") 32 | builder.WriteString(networks[i].String()) 33 | } 34 | 35 | return memFile{ 36 | data: builder.String(), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/config/type_ip.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type TypeIP struct { 9 | Value net.IP 10 | } 11 | 12 | func (t *TypeIP) Set(value string) error { 13 | ip := net.ParseIP(value) 14 | if ip == nil { 15 | return fmt.Errorf("incorret ip %s", value) 16 | } 17 | 18 | t.Value = ip 19 | 20 | return nil 21 | } 22 | 23 | func (t *TypeIP) Get(defaultValue net.IP) net.IP { 24 | if len(t.Value) == 0 { 25 | return defaultValue 26 | } 27 | 28 | return t.Value 29 | } 30 | 31 | func (t *TypeIP) UnmarshalText(data []byte) error { 32 | return t.Set(string(data)) 33 | } 34 | 35 | func (t TypeIP) MarshalText() ([]byte, error) { 36 | return []byte(t.String()), nil 37 | } 38 | 39 | func (t TypeIP) String() string { 40 | if len(t.Value) == 0 { 41 | return "" 42 | } 43 | 44 | return t.Value.String() 45 | } 46 | -------------------------------------------------------------------------------- /internal/testlib/capture_output.go: -------------------------------------------------------------------------------- 1 | package testlib 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func CaptureStdout(callback func()) string { 11 | return captureOutput(&os.Stdout, callback) 12 | } 13 | 14 | func CaptureStderr(callback func()) string { 15 | return captureOutput(&os.Stderr, callback) 16 | } 17 | 18 | func captureOutput(filefp **os.File, callback func()) string { 19 | oldFp := *filefp 20 | 21 | defer func() { 22 | *filefp = oldFp 23 | }() 24 | 25 | reader, writer, _ := os.Pipe() 26 | buf := &bytes.Buffer{} 27 | closeChan := make(chan bool) 28 | 29 | go func() { 30 | io.Copy(buf, reader) //nolint: errcheck 31 | close(closeChan) 32 | }() 33 | 34 | *filefp = writer 35 | 36 | callback() 37 | 38 | writer.Close() 39 | <-closeChan 40 | 41 | return strings.TrimSpace(buf.String()) 42 | } 43 | -------------------------------------------------------------------------------- /internal/utils/make_qr_code_url_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/utils" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type MakeQRCodeURLTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func (suite *MakeQRCodeURLTestSuite) TestSomeData() { 17 | value := utils.MakeQRCodeURL("some data") 18 | 19 | parsed, err := url.Parse(value) 20 | suite.NoError(err) 21 | 22 | suite.Equal("some data", parsed.Query().Get("data")) 23 | suite.Equal("svg", parsed.Query().Get("format")) 24 | suite.Equal("api.qrserver.com", strings.TrimPrefix(parsed.Host, "www.")) 25 | suite.Equal("v1/create-qr-code", strings.Trim(parsed.Path, "/")) 26 | } 27 | 28 | func TestMakeQRCodeURL(t *testing.T) { 29 | t.Parallel() 30 | suite.Run(t, &MakeQRCodeURLTestSuite{}) 31 | } 32 | -------------------------------------------------------------------------------- /internal/utils/net_listener.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/9seconds/mtg/v2/network" 8 | ) 9 | 10 | type Listener struct { 11 | net.Listener 12 | } 13 | 14 | func (l Listener) Accept() (net.Conn, error) { 15 | conn, err := l.Listener.Accept() 16 | if err != nil { 17 | return nil, err //nolint: wrapcheck 18 | } 19 | 20 | if err := network.SetClientSocketOptions(conn, 0); err != nil { 21 | conn.Close() 22 | 23 | return nil, fmt.Errorf("cannot set TCP options: %w", err) 24 | } 25 | 26 | return conn, nil 27 | } 28 | 29 | func NewListener(bindTo string, bufferSize int) (net.Listener, error) { 30 | base, err := net.Listen("tcp", bindTo) 31 | if err != nil { 32 | return nil, fmt.Errorf("cannot build a base listener: %w", err) 33 | } 34 | 35 | return Listener{ 36 | Listener: base, 37 | }, nil 38 | } 39 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/pools.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "hash" 7 | "sync" 8 | ) 9 | 10 | var ( 11 | sha256HasherPool = sync.Pool{ 12 | New: func() interface{} { 13 | return sha256.New() 14 | }, 15 | } 16 | bytesBufferPool = sync.Pool{ 17 | New: func() interface{} { 18 | return &bytes.Buffer{} 19 | }, 20 | } 21 | ) 22 | 23 | func acquireSha256Hasher() hash.Hash { 24 | return sha256HasherPool.Get().(hash.Hash) //nolint: forcetypeassert 25 | } 26 | 27 | func releaseSha256Hasher(h hash.Hash) { 28 | h.Reset() 29 | sha256HasherPool.Put(h) 30 | } 31 | 32 | func acquireBytesBuffer() *bytes.Buffer { 33 | return bytesBufferPool.Get().(*bytes.Buffer) //nolint: forcetypeassert 34 | } 35 | 36 | func releaseBytesBuffer(buf *bytes.Buffer) { 37 | buf.Reset() 38 | bytesBufferPool.Put(buf) 39 | } 40 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var FuzzClientHandshakeSecret = []byte{1, 2, 3} 11 | 12 | func FuzzClientHandshake(f *testing.F) { 13 | f.Add([]byte{1, 2, 3}) 14 | 15 | f.Fuzz(func(t *testing.T, frame []byte) { 16 | data := bytes.NewReader(frame) 17 | 18 | if _, _, _, err := ClientHandshake(FuzzClientHandshakeSecret, data); err != nil { 19 | return 20 | } 21 | 22 | handshake := clientHandhakeFrame{} 23 | require.Len(t, frame, handshakeFrameLen) 24 | 25 | copy(handshake.data[:], frame) 26 | 27 | decryptor := handshake.decryptor(FuzzClientHandshakeSecret) 28 | decryptor.XORKeyStream(handshake.data[:], handshake.data[:]) 29 | 30 | require.Equal(t, handshakeConnectionType, handshake.connectionType()) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /internal/config/type_metric_prefix.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | type TypeMetricPrefix struct { 9 | Value string 10 | } 11 | 12 | func (t *TypeMetricPrefix) Set(value string) error { 13 | if ok, err := regexp.MatchString("^[a-z0-9]+$", value); !ok || err != nil { 14 | return fmt.Errorf("incorrect metric prefix %s: %w", value, err) 15 | } 16 | 17 | t.Value = value 18 | 19 | return nil 20 | } 21 | 22 | func (t TypeMetricPrefix) Get(defaultValue string) string { 23 | if t.Value == "" { 24 | return defaultValue 25 | } 26 | 27 | return t.Value 28 | } 29 | 30 | func (t *TypeMetricPrefix) UnmarshalText(data []byte) error { 31 | return t.Set(string(data)) 32 | } 33 | 34 | func (t TypeMetricPrefix) MarshalText() ([]byte, error) { 35 | return []byte(t.String()), nil 36 | } 37 | 38 | func (t TypeMetricPrefix) String() string { 39 | return t.Value 40 | } 41 | -------------------------------------------------------------------------------- /mtglib/internal/telegram/address_pool.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import "math/rand" 4 | 5 | type addressPool struct { 6 | v4 [][]tgAddr 7 | v6 [][]tgAddr 8 | } 9 | 10 | func (a addressPool) isValidDC(dc int) bool { 11 | return dc > 0 && dc <= len(a.v4) && dc <= len(a.v6) 12 | } 13 | 14 | func (a addressPool) getRandomDC() int { 15 | return 1 + rand.Intn(len(a.v4)) 16 | } 17 | 18 | func (a addressPool) getV4(dc int) []tgAddr { 19 | return a.get(a.v4, dc-1) 20 | } 21 | 22 | func (a addressPool) getV6(dc int) []tgAddr { 23 | return a.get(a.v6, dc-1) 24 | } 25 | 26 | func (a addressPool) get(addresses [][]tgAddr, dc int) []tgAddr { 27 | if dc < 0 || dc >= len(addresses) { 28 | return nil 29 | } 30 | 31 | rv := make([]tgAddr, len(addresses[dc])) 32 | copy(rv, addresses[dc]) 33 | 34 | if len(rv) > 1 { 35 | rand.Shuffle(len(rv), func(i, j int) { 36 | rv[i], rv[j] = rv[j], rv[i] 37 | }) 38 | } 39 | 40 | return rv 41 | } 42 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "encoding/binary" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func FuzzServerGenerateHandshakeFrame(f *testing.F) { 11 | f.Fuzz(func(t *testing.T, arg int) { 12 | frame := generateServerHanshakeFrame() 13 | 14 | assert.NotEqualValues(t, 0xef, frame.data[0]) 15 | 16 | firstBytes := binary.LittleEndian.Uint32(frame.data[:4]) 17 | assert.NotEqualValues(t, 0x44414548, firstBytes) 18 | assert.NotEqualValues(t, 0x54534f50, firstBytes) 19 | assert.NotEqualValues(t, 0x20544547, firstBytes) 20 | assert.NotEqualValues(t, 0x4954504f, firstBytes) 21 | assert.NotEqualValues(t, 0xeeeeeeee, firstBytes) 22 | 23 | assert.NotEqualValues( 24 | t, 25 | 0, 26 | frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7]) 27 | 28 | assert.Equal(t, handshakeConnectionType, frame.connectionType()) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/testdata/client-hello-bad-fa2e46cdb33e2a1b.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1617181365, 3 | "random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=", 4 | "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=", 5 | "host": "storage.googleapis.com", 6 | "cipherSuite": 4867, 7 | "full": "AQAB/AMDXvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPsgSt2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCACq4ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFANAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgB/7oLx9JElIALsLJS91H2QNyU1H0osKwIUelVndsLyIALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 8 | } 9 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/testdata/client-hello-ok-19dfe38384b9884b.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1617181365, 3 | "random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=", 4 | "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=", 5 | "host": "storage.googleapis.com", 6 | "cipherSuite": 4867, 7 | "full": "AQAB/AMDXvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPsgSt2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgB/7oLx9JElIALsLJS91H2QNyU1H0osKwIUelVndsLyIALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 8 | } 9 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/testdata/client-hello-ok-48f8a72a56f3174a.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1617181352, 3 | "random": "oYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12os=", 4 | "sessionId": "FGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8g=", 5 | "host": "storage.googleapis.com", 6 | "cipherSuite": 4867, 7 | "full": "AQAB/AMDoYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12osgFGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8gANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAga6CocpFP8Qd4YCFR9pkaCr97po2ALj0P5nI9Nnb3UWMALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 8 | } 9 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/testdata/client-hello-ok-651054256093c6cd.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1617181352, 3 | "random": "5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1ro=", 4 | "sessionId": "jxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8=", 5 | "host": "storage.googleapis.com", 6 | "cipherSuite": 4867, 7 | "full": "AQAB/AMD5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1rogjxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgrulAaqUdKeVYM0F+pu6on/h6LBpOyzOKG4xFIKcoFk4ALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 8 | } 9 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/testdata/client-hello-ok-79d01ef18a9d2621.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1617181365, 3 | "random": "8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQ=", 4 | "sessionId": "00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAU=", 5 | "host": "storage.googleapis.com", 6 | "cipherSuite": 4867, 7 | "full": "AQAB/AMD8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQg00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAUANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAg/9P7140NtKzjyDwBf99mOy1+FjRPAPHTNQ9WxHOKpV4ALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 8 | } 9 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/testdata/client-hello-ok-7a5569f05b118145.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1617181352, 3 | "random": "zja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4=", 4 | "sessionId": "qPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnA=", 5 | "host": "storage.googleapis.com", 6 | "cipherSuite": 4867, 7 | "full": "AQAB/AMDzja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4gqPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnAANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgXviLRAqAYJ8xOLdlcsUhldI4Xl0g/s9+y2Qrd8raPEgALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 8 | } 9 | -------------------------------------------------------------------------------- /internal/config/type_port.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type TypePort struct { 9 | Value uint 10 | } 11 | 12 | func (t *TypePort) Set(value string) error { 13 | portValue, err := strconv.ParseUint(value, 10, 16) //nolint: gomnd 14 | if err != nil { 15 | return fmt.Errorf("incorrect port number (%v): %w", value, err) 16 | } 17 | 18 | if portValue == 0 { 19 | return fmt.Errorf("incorrect port number (%s)", value) 20 | } 21 | 22 | t.Value = uint(portValue) 23 | 24 | return nil 25 | } 26 | 27 | func (t TypePort) Get(defaultValue uint) uint { 28 | if t.Value == 0 { 29 | return defaultValue 30 | } 31 | 32 | return t.Value 33 | } 34 | 35 | func (t *TypePort) UnmarshalJSON(data []byte) error { 36 | return t.Set(string(data)) 37 | } 38 | 39 | func (t TypePort) MarshalJSON() ([]byte, error) { 40 | return []byte(t.String()), nil 41 | } 42 | 43 | func (t TypePort) String() string { 44 | return strconv.Itoa(int(t.Value)) 45 | } 46 | -------------------------------------------------------------------------------- /ipblocklist/files/mem_test.go: -------------------------------------------------------------------------------- 1 | package files_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/9seconds/mtg/v2/ipblocklist/files" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type MemTestSuite struct { 15 | suite.Suite 16 | } 17 | 18 | func (suite *MemTestSuite) TestOk() { 19 | _, network1, _ := net.ParseCIDR("192.168.0.1/24") 20 | _, network2, _ := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/36") 21 | 22 | file := files.NewMem([]*net.IPNet{ 23 | network1, 24 | network2, 25 | }) 26 | 27 | reader, err := file.Open(context.Background()) 28 | suite.NoError(err) 29 | 30 | data, err := io.ReadAll(reader) 31 | suite.NoError(err) 32 | 33 | strData := strings.TrimSpace(string(data)) 34 | 35 | suite.Contains(strData, "192.168.0.0/24") 36 | suite.Contains(strData, "2001:db8:8000::/36") 37 | } 38 | 39 | func TestMem(t *testing.T) { 40 | t.Parallel() 41 | suite.Run(t, &MemTestSuite{}) 42 | } 43 | -------------------------------------------------------------------------------- /internal/testlib/mtglib_network_mock.go: -------------------------------------------------------------------------------- 1 | package testlib 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/9seconds/mtg/v2/essentials" 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | type MtglibNetworkMock struct { 12 | mock.Mock 13 | } 14 | 15 | func (m *MtglibNetworkMock) Dial(network, address string) (essentials.Conn, error) { 16 | args := m.Called(network, address) 17 | 18 | return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert 19 | } 20 | 21 | func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) { 22 | args := m.Called(ctx, network, address) 23 | 24 | return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert 25 | } 26 | 27 | func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context, 28 | network, address string) (essentials.Conn, error), 29 | ) *http.Client { 30 | return m.Called(dialFunc).Get(0).(*http.Client) //nolint: forcetypeassert 31 | } 32 | -------------------------------------------------------------------------------- /logger/noop.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "github.com/9seconds/mtg/v2/mtglib" 4 | 5 | type noopLogger struct{} 6 | 7 | func (n noopLogger) Named(_ string) mtglib.Logger { return n } 8 | func (n noopLogger) BindInt(_ string, _ int) mtglib.Logger { return n } 9 | func (n noopLogger) BindStr(_, _ string) mtglib.Logger { return n } 10 | func (n noopLogger) BindJSON(_, _ string) mtglib.Logger { return n } 11 | func (n noopLogger) Printf(_ string, _ ...interface{}) {} 12 | func (n noopLogger) Info(_ string) {} 13 | func (n noopLogger) Warning(_ string) {} 14 | func (n noopLogger) Debug(_ string) {} 15 | func (n noopLogger) InfoError(_ string, _ error) {} 16 | func (n noopLogger) WarningError(_ string, _ error) {} 17 | func (n noopLogger) DebugError(_ string, _ error) {} 18 | 19 | // NewNoopLogger returns a logger which discards all events. 20 | func NewNoopLogger() mtglib.Logger { 21 | return noopLogger{} 22 | } 23 | -------------------------------------------------------------------------------- /mtglib/init_internal_test.go: -------------------------------------------------------------------------------- 1 | package mtglib 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type NoopLogger struct{} 10 | 11 | func (n NoopLogger) Named(_ string) Logger { return n } 12 | func (n NoopLogger) BindInt(_ string, _ int) Logger { return n } 13 | func (n NoopLogger) BindStr(_, _ string) Logger { return n } 14 | func (n NoopLogger) BindJSON(_, _ string) Logger { return n } 15 | func (n NoopLogger) Printf(_ string, _ ...interface{}) {} 16 | func (n NoopLogger) Info(_ string) {} 17 | func (n NoopLogger) Warning(_ string) {} 18 | func (n NoopLogger) Debug(_ string) {} 19 | func (n NoopLogger) InfoError(_ string, _ error) {} 20 | func (n NoopLogger) WarningError(_ string, _ error) {} 21 | func (n NoopLogger) DebugError(_ string, _ error) {} 22 | 23 | type EventStreamMock struct { 24 | mock.Mock 25 | } 26 | 27 | func (e *EventStreamMock) Send(ctx context.Context, evt Event) { 28 | e.Called(ctx, evt) 29 | } 30 | -------------------------------------------------------------------------------- /internal/config/type_concurrency.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type TypeConcurrency struct { 9 | Value uint 10 | } 11 | 12 | func (t *TypeConcurrency) Set(value string) error { 13 | concurrencyValue, err := strconv.ParseUint(value, 10, 16) //nolint: gomnd 14 | if err != nil { 15 | return fmt.Errorf("value is not uint (%s): %w", value, err) 16 | } 17 | 18 | if concurrencyValue == 0 { 19 | return fmt.Errorf("value should be >0 (%s)", value) 20 | } 21 | 22 | t.Value = uint(concurrencyValue) 23 | 24 | return nil 25 | } 26 | 27 | func (t TypeConcurrency) Get(defaultValue uint) uint { 28 | if t.Value == 0 { 29 | return defaultValue 30 | } 31 | 32 | return t.Value 33 | } 34 | 35 | func (t *TypeConcurrency) UnmarshalJSON(data []byte) error { 36 | return t.Set(string(data)) 37 | } 38 | 39 | func (t TypeConcurrency) MarshalJSON() ([]byte, error) { 40 | return []byte(t.String()), nil 41 | } 42 | 43 | func (t TypeConcurrency) String() string { 44 | return strconv.FormatUint(uint64(t.Value), 10) //nolint: gomnd 45 | } 46 | -------------------------------------------------------------------------------- /logger/noop_test.go: -------------------------------------------------------------------------------- 1 | package logger_test 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/testlib" 8 | "github.com/9seconds/mtg/v2/logger" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type NoopLoggerTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func (suite *NoopLoggerTestSuite) TestLog() { 17 | suite.Empty(testlib.CaptureStdout(func() { 18 | suite.Empty(testlib.CaptureStderr(func() { 19 | log := logger.NewNoopLogger().Named("name") 20 | 21 | log.BindInt("int", 1).BindStr("str", "1").Printf("info", 1, 2) 22 | log.BindInt("int", 1).BindStr("str", "1").Info("info") 23 | log.BindInt("int", 1).BindStr("str", "1").Warning("info") 24 | log.BindInt("int", 1).BindStr("str", "1").Debug("info") 25 | log.BindInt("int", 1).BindStr("str", "1").InfoError("info", io.EOF) 26 | log.BindInt("int", 1).BindStr("str", "1").WarningError("info", io.EOF) 27 | log.BindInt("int", 1).BindStr("str", "1").DebugError("info", io.EOF) 28 | })) 29 | })) 30 | } 31 | 32 | func TestNoopLogger(t *testing.T) { 33 | t.Parallel() 34 | suite.Run(t, &NoopLoggerTestSuite{}) 35 | } 36 | -------------------------------------------------------------------------------- /network/proxy_dialer.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | func newProxyDialer(baseDialer Dialer, proxyURL *url.URL) Dialer { 10 | params := proxyURL.Query() 11 | 12 | var ( 13 | openThreshold uint32 = ProxyDialerOpenThreshold 14 | halfOpenTimeout = ProxyDialerHalfOpenTimeout 15 | resetFailuresTimeout = ProxyDialerResetFailuresTimeout 16 | ) 17 | 18 | if param := params.Get("open_threshold"); param != "" { 19 | if intNum, err := strconv.ParseUint(param, 10, 32); err == nil { //nolint: gomnd 20 | openThreshold = uint32(intNum) 21 | } 22 | } 23 | 24 | if param := params.Get("half_open_timeout"); param != "" { 25 | if dur, err := time.ParseDuration(param); err == nil && dur > 0 { 26 | halfOpenTimeout = dur 27 | } 28 | } 29 | 30 | if param := params.Get("reset_failures_timeout"); param != "" { 31 | if dur, err := time.ParseDuration(param); err == nil && dur > 0 { 32 | resetFailuresTimeout = dur 33 | } 34 | } 35 | 36 | return newCircuitBreakerDialer(baseDialer, openThreshold, halfOpenTimeout, resetFailuresTimeout) 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sergey Arkhipov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/config/type_error_rate.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | const typeErrorRateIgnoreLess = 1e-8 9 | 10 | type TypeErrorRate struct { 11 | Value float64 12 | } 13 | 14 | func (t *TypeErrorRate) Set(value string) error { 15 | parsedValue, err := strconv.ParseFloat(value, 64) //nolint: gomnd 16 | if err != nil { 17 | return fmt.Errorf("value is not a float (%s): %w", value, err) 18 | } 19 | 20 | if parsedValue <= 0.0 || parsedValue >= 100.0 { 21 | return fmt.Errorf("value should be 0 < x < 100 (%s)", value) 22 | } 23 | 24 | t.Value = parsedValue 25 | 26 | return nil 27 | } 28 | 29 | func (t TypeErrorRate) Get(defaultValue float64) float64 { 30 | if t.Value < typeErrorRateIgnoreLess { 31 | return defaultValue 32 | } 33 | 34 | return t.Value 35 | } 36 | 37 | func (t *TypeErrorRate) UnmarshalJSON(data []byte) error { 38 | return t.Set(string(data)) 39 | } 40 | 41 | func (t TypeErrorRate) MarshalJSON() ([]byte, error) { 42 | return []byte(t.String()), nil 43 | } 44 | 45 | func (t TypeErrorRate) String() string { 46 | return strconv.FormatFloat(t.Value, 'f', -1, 64) //nolint: gomnd 47 | } 48 | -------------------------------------------------------------------------------- /network/dns_resolver_internal_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type DNSResolverTestSuite struct { 13 | suite.Suite 14 | 15 | d *dnsResolver 16 | } 17 | 18 | func (suite *DNSResolverTestSuite) TestLookupA() { 19 | suite.d.LookupA("google.com") 20 | time.Sleep(10 * time.Millisecond) 21 | 22 | addrs := suite.d.LookupA("google.com") 23 | 24 | for _, v := range addrs { 25 | suite.NotEmpty(v) 26 | suite.NotNil(net.ParseIP(v).To4()) 27 | } 28 | } 29 | 30 | func (suite *DNSResolverTestSuite) TestLookupAAAA() { 31 | suite.d.LookupAAAA("google.com") 32 | time.Sleep(10 * time.Millisecond) 33 | 34 | addrs := suite.d.LookupAAAA("google.com") 35 | 36 | for _, v := range addrs { 37 | suite.NotEmpty(v) 38 | suite.Nil(net.ParseIP(v).To4()) 39 | suite.NotNil(net.ParseIP(v).To16()) 40 | } 41 | } 42 | 43 | func (suite *DNSResolverTestSuite) SetupTest() { 44 | suite.d = newDNSResolver("1.1.1.1", &http.Client{}) 45 | } 46 | 47 | func TestDNSResolver(t *testing.T) { 48 | t.Parallel() 49 | suite.Run(t, &DNSResolverTestSuite{}) 50 | } 51 | -------------------------------------------------------------------------------- /internal/config/type_duration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | var typeDurationStringCleaner = strings.NewReplacer(" ", "", "\t", "") 10 | 11 | type TypeDuration struct { 12 | Value time.Duration 13 | } 14 | 15 | func (t *TypeDuration) Set(value string) error { 16 | parsedValue, err := time.ParseDuration( 17 | typeDurationStringCleaner.Replace(strings.ToLower(value))) 18 | if err != nil { 19 | return fmt.Errorf("incorrect duration (%s): %w", value, err) 20 | } 21 | 22 | if parsedValue < 0 { 23 | return fmt.Errorf("duration has to be a positive: %s", value) 24 | } 25 | 26 | t.Value = parsedValue 27 | 28 | return nil 29 | } 30 | 31 | func (t TypeDuration) Get(defaultValue time.Duration) time.Duration { 32 | if t.Value == 0 { 33 | return defaultValue 34 | } 35 | 36 | return t.Value 37 | } 38 | 39 | func (t *TypeDuration) UnmarshalText(data []byte) error { 40 | return t.Set(string(data)) 41 | } 42 | 43 | func (t TypeDuration) MarshalText() ([]byte, error) { 44 | return []byte(t.String()), nil 45 | } 46 | 47 | func (t TypeDuration) String() string { 48 | if t.Value == 0 { 49 | return "" 50 | } 51 | 52 | return t.Value.String() 53 | } 54 | -------------------------------------------------------------------------------- /events/init_test.go: -------------------------------------------------------------------------------- 1 | package events_test 2 | 3 | import ( 4 | "github.com/9seconds/mtg/v2/mtglib" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type ObserverMock struct { 9 | mock.Mock 10 | } 11 | 12 | func (o *ObserverMock) EventStart(evt mtglib.EventStart) { 13 | o.Called(evt) 14 | } 15 | 16 | func (o *ObserverMock) EventConnectedToDC(evt mtglib.EventConnectedToDC) { 17 | o.Called(evt) 18 | } 19 | 20 | func (o *ObserverMock) EventDomainFronting(evt mtglib.EventDomainFronting) { 21 | o.Called(evt) 22 | } 23 | 24 | func (o *ObserverMock) EventTraffic(evt mtglib.EventTraffic) { 25 | o.Called(evt) 26 | } 27 | 28 | func (o *ObserverMock) EventFinish(evt mtglib.EventFinish) { 29 | o.Called(evt) 30 | } 31 | 32 | func (o *ObserverMock) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) { 33 | o.Called(evt) 34 | } 35 | 36 | func (o *ObserverMock) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) { 37 | o.Called(evt) 38 | } 39 | 40 | func (o *ObserverMock) EventReplayAttack(evt mtglib.EventReplayAttack) { 41 | o.Called(evt) 42 | } 43 | 44 | func (o *ObserverMock) EventIPListSize(evt mtglib.EventIPListSize) { 45 | o.Called(evt) 46 | } 47 | 48 | func (o *ObserverMock) Shutdown() { 49 | o.Called() 50 | } 51 | -------------------------------------------------------------------------------- /internal/config/type_bytes.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/alecthomas/units" 8 | ) 9 | 10 | var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB") 11 | 12 | type TypeBytes struct { 13 | Value units.Base2Bytes 14 | } 15 | 16 | func (t *TypeBytes) Set(value string) error { 17 | normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value)) 18 | 19 | parsedValue, err := units.ParseBase2Bytes(normalizedValue) 20 | if err != nil { 21 | return fmt.Errorf("incorrect bytes value (%v): %w", value, err) 22 | } 23 | 24 | if parsedValue < 0 { 25 | return fmt.Errorf("bytes should be positive (%s)", value) 26 | } 27 | 28 | t.Value = parsedValue 29 | 30 | return nil 31 | } 32 | 33 | func (t TypeBytes) Get(defaultValue uint) uint { 34 | if t.Value == 0 { 35 | return defaultValue 36 | } 37 | 38 | return uint(t.Value) 39 | } 40 | 41 | func (t *TypeBytes) UnmarshalText(data []byte) error { 42 | return t.Set(string(data)) 43 | } 44 | 45 | func (t TypeBytes) MarshalText() ([]byte, error) { 46 | return []byte(t.String()), nil 47 | } 48 | 49 | func (t TypeBytes) String() string { 50 | if t.Value == 0 { 51 | return "" 52 | } 53 | 54 | return strings.ToLower(t.Value.String()) 55 | } 56 | -------------------------------------------------------------------------------- /ipblocklist/files/local_test.go: -------------------------------------------------------------------------------- 1 | package files_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/9seconds/mtg/v2/ipblocklist/files" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/suite" 13 | ) 14 | 15 | type LocalTestSuite struct { 16 | suite.Suite 17 | } 18 | 19 | func (suite *LocalTestSuite) getLocalFile(name string) string { 20 | return filepath.Join("testdata", name) 21 | } 22 | 23 | func (suite *LocalTestSuite) TestIncorrect() { 24 | names := []string{ 25 | "absent", 26 | "directory", 27 | } 28 | 29 | for _, v := range names { 30 | value := v 31 | 32 | suite.T().Run(v, func(t *testing.T) { 33 | _, err := files.NewLocal(suite.getLocalFile(value)) 34 | assert.Error(t, err) 35 | }) 36 | } 37 | } 38 | 39 | func (suite *LocalTestSuite) TestOk() { 40 | file, err := files.NewLocal(suite.getLocalFile("readable")) 41 | suite.NoError(err) 42 | 43 | reader, err := file.Open(context.Background()) 44 | suite.NoError(err) 45 | 46 | data, err := io.ReadAll(reader) 47 | suite.NoError(err) 48 | 49 | suite.Equal("Hooray!", strings.TrimSpace(string(data))) 50 | } 51 | 52 | func TestLocal(t *testing.T) { 53 | t.Parallel() 54 | suite.Run(t, &LocalTestSuite{}) 55 | } 56 | -------------------------------------------------------------------------------- /events/noop.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/9seconds/mtg/v2/mtglib" 7 | ) 8 | 9 | type noop struct{} 10 | 11 | func (n noop) Send(ctx context.Context, evt mtglib.Event) {} 12 | 13 | // NewNoopStream creates a stream which discards each message. 14 | func NewNoopStream() mtglib.EventStream { 15 | return noop{} 16 | } 17 | 18 | type noopObserver struct{} 19 | 20 | func (n noopObserver) EventStart(_ mtglib.EventStart) {} 21 | func (n noopObserver) EventConnectedToDC(_ mtglib.EventConnectedToDC) {} 22 | func (n noopObserver) EventDomainFronting(_ mtglib.EventDomainFronting) {} 23 | func (n noopObserver) EventTraffic(_ mtglib.EventTraffic) {} 24 | func (n noopObserver) EventFinish(_ mtglib.EventFinish) {} 25 | func (n noopObserver) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimited) {} 26 | func (n noopObserver) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) {} 27 | func (n noopObserver) EventReplayAttack(_ mtglib.EventReplayAttack) {} 28 | func (n noopObserver) EventIPListSize(_ mtglib.EventIPListSize) {} 29 | func (n noopObserver) Shutdown() {} 30 | 31 | // NewNoopObserver creates an observer which discards each message. 32 | func NewNoopObserver() Observer { 33 | return noopObserver{} 34 | } 35 | -------------------------------------------------------------------------------- /internal/config/type_proxy_url.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | ) 8 | 9 | const typeProxyURLDefaultSOCKS5Port = "1080" 10 | 11 | type TypeProxyURL struct { 12 | Value *url.URL 13 | } 14 | 15 | func (t *TypeProxyURL) Set(value string) error { 16 | parsedURL, err := url.Parse(value) 17 | if err != nil { 18 | return fmt.Errorf("value is not corect URL (%s): %w", value, err) 19 | } 20 | 21 | if parsedURL.Host == "" { 22 | return fmt.Errorf("url has to have a schema: %s", value) 23 | } 24 | 25 | if parsedURL.Scheme != "socks5" { 26 | return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme) 27 | } 28 | 29 | if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil { 30 | parsedURL.Host = net.JoinHostPort(parsedURL.Host, 31 | typeProxyURLDefaultSOCKS5Port) 32 | } 33 | 34 | t.Value = parsedURL 35 | 36 | return nil 37 | } 38 | 39 | func (t *TypeProxyURL) Get(defaultValue *url.URL) *url.URL { 40 | if t.Value == nil { 41 | return defaultValue 42 | } 43 | 44 | return t.Value 45 | } 46 | 47 | func (t *TypeProxyURL) UnmarshalText(data []byte) error { 48 | return t.Set(string(data)) 49 | } 50 | 51 | func (t TypeProxyURL) MarshalText() ([]byte, error) { 52 | return []byte(t.String()), nil 53 | } 54 | 55 | func (t TypeProxyURL) String() string { 56 | if t.Value == nil { 57 | return "" 58 | } 59 | 60 | return t.Value.String() 61 | } 62 | -------------------------------------------------------------------------------- /mtglib/internal/relay/relay.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | "github.com/9seconds/mtg/v2/essentials" 9 | ) 10 | 11 | func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.Conn) { 12 | defer telegramConn.Close() 13 | defer clientConn.Close() 14 | 15 | ctx, cancel := context.WithCancel(ctx) 16 | defer cancel() 17 | 18 | go func() { 19 | <-ctx.Done() 20 | telegramConn.Close() 21 | clientConn.Close() 22 | }() 23 | 24 | closeChan := make(chan struct{}) 25 | 26 | go func() { 27 | defer close(closeChan) 28 | 29 | pump(log, telegramConn, clientConn, "client -> telegram") 30 | }() 31 | 32 | pump(log, clientConn, telegramConn, "telegram -> client") 33 | 34 | <-closeChan 35 | } 36 | 37 | func pump(log Logger, src, dst essentials.Conn, direction string) { 38 | defer src.CloseRead() //nolint: errcheck 39 | defer dst.CloseWrite() //nolint: errcheck 40 | 41 | copyBuffer := acquireCopyBuffer() 42 | defer releaseCopyBuffer(copyBuffer) 43 | 44 | n, err := io.CopyBuffer(src, dst, *copyBuffer) 45 | 46 | switch { 47 | case err == nil: 48 | log.Printf("%s has been finished", direction) 49 | case errors.Is(err, io.EOF): 50 | log.Printf("%s has been finished because of EOF. Written %d bytes", direction, n) 51 | default: 52 | log.Printf("%s has been finished (written %d bytes): %v", direction, n, err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | project_name: mtg 4 | 5 | before: 6 | hooks: 7 | - go mod tidy 8 | - go generate ./... 9 | 10 | builds: 11 | - binary: '{{ .ProjectName }}' 12 | goos: 13 | - darwin 14 | - freebsd 15 | - linux 16 | - netbsd 17 | - openbsd 18 | goarch: 19 | - 386 20 | - amd64 21 | - arm 22 | - arm64 23 | goarm: 24 | - 6 25 | - 7 26 | env: 27 | - CGO_ENABLED=0 28 | flags: 29 | - -trimpath 30 | - -mod=readonly 31 | ldflags: -s -w -X main.version={{ .Version }} 32 | ignore: 33 | - goos: darwin 34 | goarch: 386 35 | - goos: freebsd 36 | goarch: arm64 37 | - goos: netbsd 38 | goarch: arm64 39 | - goos: openbsd 40 | goarch: arm64 41 | 42 | archives: 43 | - name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 44 | format: tar.gz 45 | wrap_in_directory: true 46 | format_overrides: 47 | - goos: windows 48 | format: zip 49 | files: 50 | - LICENSE 51 | - README.md 52 | - SECURITY.md 53 | 54 | gomod: 55 | proxy: true 56 | 57 | snapshot: 58 | name_template: '{{ .Version }}' 59 | 60 | checksum: 61 | name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt' 62 | 63 | source: 64 | enabled: true 65 | name_template: '{{ .ProjectName }}-sources' 66 | -------------------------------------------------------------------------------- /mtglib/conns.go: -------------------------------------------------------------------------------- 1 | package mtglib 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "sync" 8 | 9 | "github.com/9seconds/mtg/v2/essentials" 10 | ) 11 | 12 | type connTraffic struct { 13 | essentials.Conn 14 | 15 | streamID string 16 | stream EventStream 17 | ctx context.Context 18 | } 19 | 20 | func (c connTraffic) Read(b []byte) (int, error) { 21 | n, err := c.Conn.Read(b) 22 | 23 | if n > 0 { 24 | c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), true)) 25 | } 26 | 27 | return n, err //nolint: wrapcheck 28 | } 29 | 30 | func (c connTraffic) Write(b []byte) (int, error) { 31 | n, err := c.Conn.Write(b) 32 | 33 | if n > 0 { 34 | c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), false)) 35 | } 36 | 37 | return n, err //nolint: wrapcheck 38 | } 39 | 40 | type connRewind struct { 41 | essentials.Conn 42 | 43 | active io.Reader 44 | buf bytes.Buffer 45 | mutex sync.RWMutex 46 | } 47 | 48 | func (c *connRewind) Read(p []byte) (int, error) { 49 | c.mutex.RLock() 50 | defer c.mutex.RUnlock() 51 | 52 | return c.active.Read(p) //nolint: wrapcheck 53 | } 54 | 55 | func (c *connRewind) Rewind() { 56 | c.mutex.Lock() 57 | defer c.mutex.Unlock() 58 | 59 | c.active = io.MultiReader(&c.buf, c.Conn) 60 | } 61 | 62 | func newConnRewind(conn essentials.Conn) *connRewind { 63 | rv := &connRewind{ 64 | Conn: conn, 65 | } 66 | rv.active = io.TeeReader(conn, &rv.buf) 67 | 68 | return rv 69 | } 70 | -------------------------------------------------------------------------------- /network/sockopts.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // SetClientSocketOptions tunes a TCP socket that represents a connection to 9 | // end user (not Telegram service or fronting domain). 10 | // 11 | // bufferSize setting is deprecated and ignored. 12 | func SetClientSocketOptions(conn net.Conn, bufferSize int) error { 13 | return setCommonSocketOptions(conn.(*net.TCPConn)) //nolint: forcetypeassert 14 | } 15 | 16 | // SetServerSocketOptions tunes a TCP socket that represents a connection to 17 | // remote server like Telegram or fronting domain (but not end user). 18 | func SetServerSocketOptions(conn net.Conn, bufferSize int) error { 19 | return setCommonSocketOptions(conn.(*net.TCPConn)) //nolint: forcetypeassert 20 | } 21 | 22 | func setCommonSocketOptions(conn *net.TCPConn) error { 23 | if err := conn.SetKeepAlivePeriod(DefaultTCPKeepAlivePeriod); err != nil { 24 | return fmt.Errorf("cannot set time period of TCP keepalive probes: %w", err) 25 | } 26 | 27 | if err := conn.SetLinger(tcpLingerTimeout); err != nil { 28 | return fmt.Errorf("cannot set TCP linger timeout: %w", err) 29 | } 30 | 31 | rawConn, err := conn.SyscallConn() 32 | if err != nil { 33 | return fmt.Errorf("cannot get underlying raw connection: %w", err) 34 | } 35 | 36 | if err := setSocketReuseAddrPort(rawConn); err != nil { 37 | return fmt.Errorf("cannot setup SO_REUSEADDR/PORT: %w", err) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/config/type_hostport.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | ) 8 | 9 | type TypeHostPort struct { 10 | Value string 11 | Host string 12 | Port uint 13 | } 14 | 15 | func (t *TypeHostPort) Set(value string) error { 16 | host, port, err := net.SplitHostPort(value) 17 | if err != nil { 18 | return fmt.Errorf("incorrect host:port value (%v): %w", value, err) 19 | } 20 | 21 | portValue, err := strconv.ParseUint(port, 10, 16) //nolint: gomnd 22 | if err != nil { 23 | return fmt.Errorf("incorrect port number (%v): %w", value, err) 24 | } 25 | 26 | if portValue == 0 { 27 | return fmt.Errorf("incorrect port number (%s)", value) 28 | } 29 | 30 | if host == "" { 31 | return fmt.Errorf("empty host: %s", value) 32 | } 33 | 34 | if net.ParseIP(host) == nil { 35 | return fmt.Errorf("host is not an IP address: %s", value) 36 | } 37 | 38 | t.Value = net.JoinHostPort(host, port) 39 | t.Port = uint(portValue) 40 | t.Host = host 41 | 42 | return nil 43 | } 44 | 45 | func (t TypeHostPort) Get(defaultValue string) string { 46 | if t.Value == "" { 47 | return defaultValue 48 | } 49 | 50 | return t.Value 51 | } 52 | 53 | func (t *TypeHostPort) UnmarshalText(data []byte) error { 54 | return t.Set(string(data)) 55 | } 56 | 57 | func (t TypeHostPort) MarshalText() ([]byte, error) { 58 | return []byte(t.String()), nil 59 | } 60 | 61 | func (t TypeHostPort) String() string { 62 | return t.Value 63 | } 64 | -------------------------------------------------------------------------------- /internal/config/type_statsd_tag_format.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // TypeStatsdTagFormatInfluxdb defines a tag format compatible with 10 | // InfluxDB. 11 | TypeStatsdTagFormatInfluxdb = "influxdb" 12 | 13 | // TypeStatsdTagFormatDatadog defines a tag format compatible with 14 | // DataDog. 15 | TypeStatsdTagFormatDatadog = "datadog" 16 | 17 | // TypeStatsdTagFormatGraphite defines a tag format compatible with 18 | // Graphite. 19 | TypeStatsdTagFormatGraphite = "graphite" 20 | ) 21 | 22 | type TypeStatsdTagFormat struct { 23 | Value string 24 | } 25 | 26 | func (t *TypeStatsdTagFormat) Set(value string) error { 27 | lowercasedValue := strings.ToLower(value) 28 | 29 | switch lowercasedValue { 30 | case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb, 31 | TypeStatsdTagFormatGraphite: 32 | t.Value = lowercasedValue 33 | 34 | return nil 35 | default: 36 | return fmt.Errorf("unknown tag format %s", value) 37 | } 38 | } 39 | 40 | func (t TypeStatsdTagFormat) Get(defaultValue string) string { 41 | if t.Value == "" { 42 | return defaultValue 43 | } 44 | 45 | return t.Value 46 | } 47 | 48 | func (t *TypeStatsdTagFormat) UnmarshalText(data []byte) error { 49 | return t.Set(string(data)) 50 | } 51 | 52 | func (t *TypeStatsdTagFormat) MarshalText() ([]byte, error) { 53 | return []byte(t.String()), nil 54 | } 55 | 56 | func (t *TypeStatsdTagFormat) String() string { 57 | return t.Value 58 | } 59 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/config" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type ConfigTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func (suite *ConfigTestSuite) ReadConfig(filename string) []byte { 17 | data, err := os.ReadFile(filepath.Join("testdata", filename)) 18 | suite.NoError(err) 19 | 20 | return data 21 | } 22 | 23 | func (suite *ConfigTestSuite) TestParseEmpty() { 24 | _, err := config.Parse([]byte{}) 25 | suite.Error(err) 26 | } 27 | 28 | func (suite *ConfigTestSuite) TestParseBrokenToml() { 29 | _, err := config.Parse(suite.ReadConfig("broken.toml")) 30 | suite.Error(err) 31 | } 32 | 33 | func (suite *ConfigTestSuite) TestParseOnlySecret() { 34 | _, err := config.Parse(suite.ReadConfig("only_secret.toml")) 35 | suite.Error(err) 36 | } 37 | 38 | func (suite *ConfigTestSuite) TestParseMinimalConfig() { 39 | conf, err := config.Parse(suite.ReadConfig("minimal.toml")) 40 | suite.NoError(err) 41 | suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64()) 42 | suite.Equal("0.0.0.0:3128", conf.BindTo.String()) 43 | } 44 | 45 | func (suite *ConfigTestSuite) TestString() { 46 | conf, err := config.Parse(suite.ReadConfig("minimal.toml")) 47 | suite.NoError(err) 48 | suite.NotEmpty(conf.String()) 49 | } 50 | 51 | func TestConfig(t *testing.T) { 52 | t.Parallel() 53 | suite.Run(t, &ConfigTestSuite{}) 54 | } 55 | -------------------------------------------------------------------------------- /internal/testlib/net_conn_mock.go: -------------------------------------------------------------------------------- 1 | package testlib 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | type EssentialsConnMock struct { 11 | mock.Mock 12 | } 13 | 14 | func (n *EssentialsConnMock) Read(b []byte) (int, error) { 15 | args := n.Called(b) 16 | 17 | return args.Int(0), args.Error(1) 18 | } 19 | 20 | func (n *EssentialsConnMock) Write(b []byte) (int, error) { 21 | args := n.Called(b) 22 | 23 | return args.Int(0), args.Error(1) 24 | } 25 | 26 | func (n *EssentialsConnMock) Close() error { 27 | return n.Called().Error(0) //nolint: wrapcheck 28 | } 29 | 30 | func (n *EssentialsConnMock) CloseRead() error { 31 | return n.Called().Error(0) //nolint: wrapcheck 32 | } 33 | 34 | func (n *EssentialsConnMock) CloseWrite() error { 35 | return n.Called().Error(0) //nolint: wrapcheck 36 | } 37 | 38 | func (n *EssentialsConnMock) LocalAddr() net.Addr { 39 | return n.Called().Get(0).(net.Addr) //nolint: forcetypeassert 40 | } 41 | 42 | func (n *EssentialsConnMock) RemoteAddr() net.Addr { 43 | return n.Called().Get(0).(net.Addr) //nolint: forcetypeassert 44 | } 45 | 46 | func (n *EssentialsConnMock) SetDeadline(t time.Time) error { 47 | return n.Called(t).Error(0) //nolint: wrapcheck 48 | } 49 | 50 | func (n *EssentialsConnMock) SetReadDeadline(t time.Time) error { 51 | return n.Called(t).Error(0) //nolint: wrapcheck 52 | } 53 | 54 | func (n *EssentialsConnMock) SetWriteDeadline(t time.Time) error { 55 | return n.Called(t).Error(0) //nolint: wrapcheck 56 | } 57 | -------------------------------------------------------------------------------- /internal/config/type_prefer_ip.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // TypePreferIPPreferIPv4 states that you prefer to use IPv4 addresses 10 | // but IPv6 is also possible. 11 | TypePreferIPPreferIPv4 = "prefer-ipv4" 12 | 13 | // TypePreferIPPreferIPv6 states that you prefer to use IPv6 addresses 14 | // but IPv4 is also possible. 15 | TypePreferIPPreferIPv6 = "prefer-ipv6" 16 | 17 | // TypePreferOnlyIPv4 states that you prefer to use IPv4 addresses 18 | // only. 19 | TypePreferOnlyIPv4 = "only-ipv4" 20 | 21 | // TypePreferOnlyIPv6 states that you prefer to use IPv6 addresses 22 | // only. 23 | TypePreferOnlyIPv6 = "only-ipv6" 24 | ) 25 | 26 | type TypePreferIP struct { 27 | Value string 28 | } 29 | 30 | func (t *TypePreferIP) Set(value string) error { 31 | value = strings.ToLower(value) 32 | 33 | switch value { 34 | case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, 35 | TypePreferOnlyIPv4, TypePreferOnlyIPv6: 36 | t.Value = value 37 | 38 | return nil 39 | default: 40 | return fmt.Errorf("unsupported ip preference: %s", value) 41 | } 42 | } 43 | 44 | func (t *TypePreferIP) Get(defaultValue string) string { 45 | if t.Value == "" { 46 | return defaultValue 47 | } 48 | 49 | return t.Value 50 | } 51 | 52 | func (t *TypePreferIP) UnmarshalText(data []byte) error { 53 | return t.Set(string(data)) 54 | } 55 | 56 | func (t TypePreferIP) MarshalText() ([]byte, error) { 57 | return []byte(t.String()), nil 58 | } 59 | 60 | func (t TypePreferIP) String() string { 61 | return t.Value 62 | } 63 | -------------------------------------------------------------------------------- /ipblocklist/files/http.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | type httpFile struct { 12 | http *http.Client 13 | url string 14 | } 15 | 16 | func (h httpFile) Open(ctx context.Context) (io.ReadCloser, error) { 17 | request, err := http.NewRequestWithContext(ctx, http.MethodGet, h.url, nil) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | response, err := h.http.Do(request) 23 | if err != nil { 24 | if response != nil { 25 | io.Copy(io.Discard, response.Body) //nolint: errcheck 26 | response.Body.Close() 27 | } 28 | 29 | return nil, fmt.Errorf("cannot get url %s: %w", h.url, err) 30 | } 31 | 32 | if response.StatusCode >= http.StatusBadRequest { 33 | return nil, fmt.Errorf("unexpected status code %d", response.StatusCode) 34 | } 35 | 36 | return response.Body, nil 37 | } 38 | 39 | func (h httpFile) String() string { 40 | return h.url 41 | } 42 | 43 | // NewHTTP returns a file abstraction for HTTP/HTTPS endpoint. You also need to 44 | // provide a valid instance of [http.Client] to access it. 45 | func NewHTTP(client *http.Client, endpoint string) (File, error) { 46 | if client == nil { 47 | return nil, ErrBadHTTPClient 48 | } 49 | 50 | parsed, err := url.Parse(endpoint) 51 | if err != nil { 52 | return nil, fmt.Errorf("incorrect url %s: %w", endpoint, err) 53 | } 54 | 55 | switch parsed.Scheme { 56 | case "http", "https": 57 | default: 58 | return nil, fmt.Errorf("unsupported url %s", endpoint) 59 | } 60 | 61 | return httpFile{ 62 | http: client, 63 | url: endpoint, 64 | }, nil 65 | } 66 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/client_handshake.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "crypto/cipher" 5 | "crypto/subtle" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | type clientHandhakeFrame struct { 12 | handshakeFrame 13 | } 14 | 15 | func (c *clientHandhakeFrame) decryptor(secret []byte) cipher.Stream { 16 | hasher := acquireSha256Hasher() 17 | defer releaseSha256Hasher(hasher) 18 | 19 | hasher.Write(c.key()) 20 | hasher.Write(secret) 21 | 22 | return makeAesCtr(hasher.Sum(nil), c.iv()) 23 | } 24 | 25 | func (c *clientHandhakeFrame) encryptor(secret []byte) cipher.Stream { 26 | invertedHandshake := c.invert() 27 | 28 | hasher := acquireSha256Hasher() 29 | defer releaseSha256Hasher(hasher) 30 | 31 | hasher.Write(invertedHandshake.key()) 32 | hasher.Write(secret) 33 | 34 | return makeAesCtr(hasher.Sum(nil), invertedHandshake.iv()) 35 | } 36 | 37 | func ClientHandshake(secret []byte, reader io.Reader) (int, cipher.Stream, cipher.Stream, error) { 38 | handshake := clientHandhakeFrame{} 39 | 40 | if _, err := io.ReadFull(reader, handshake.data[:]); err != nil { 41 | return 0, nil, nil, fmt.Errorf("cannot read frame: %w", err) 42 | } 43 | 44 | decryptor := handshake.decryptor(secret) 45 | encryptor := handshake.encryptor(secret) 46 | 47 | decryptor.XORKeyStream(handshake.data[:], handshake.data[:]) 48 | 49 | if val := handshake.connectionType(); subtle.ConstantTimeCompare(handshakeConnectionType, val) != 1 { 50 | return 0, nil, nil, fmt.Errorf("unsupported connection type: %s", hex.EncodeToString(val)) 51 | } 52 | 53 | return handshake.dc(), encryptor, decryptor, nil 54 | } 55 | -------------------------------------------------------------------------------- /network/socks5_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/network" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type Socks5TestSuite struct { 12 | suite.Suite 13 | HTTPServerTestSuite 14 | Socks5ServerTestSuite 15 | 16 | d network.Dialer 17 | } 18 | 19 | func (suite *Socks5TestSuite) SetupSuite() { 20 | suite.HTTPServerTestSuite.SetupSuite() 21 | suite.Socks5ServerTestSuite.SetupSuite() 22 | 23 | suite.d, _ = network.NewDefaultDialer(0, 0) 24 | } 25 | 26 | func (suite *Socks5TestSuite) TearDownSuite() { 27 | suite.Socks5ServerTestSuite.TearDownSuite() 28 | suite.HTTPServerTestSuite.TearDownSuite() 29 | } 30 | 31 | func (suite *Socks5TestSuite) TestRequestFailed() { 32 | proxyURL := suite.MakeSocks5URL("user2", "password") 33 | dialer, _ := network.NewSocks5Dialer(suite.d, proxyURL) 34 | httpClient := suite.MakeHTTPClient(dialer) 35 | 36 | resp, err := httpClient.Get(suite.MakeURL("/get")) //nolint: noctx 37 | if err == nil { 38 | defer resp.Body.Close() 39 | } 40 | 41 | suite.Error(err) 42 | } 43 | 44 | func (suite *Socks5TestSuite) TestRequestOk() { 45 | proxyURL := suite.MakeSocks5URL("user", "password") 46 | dialer, _ := network.NewSocks5Dialer(suite.d, proxyURL) 47 | httpClient := suite.MakeHTTPClient(dialer) 48 | 49 | resp, err := httpClient.Get(suite.MakeURL("/get")) //nolint: noctx 50 | if err == nil { 51 | defer resp.Body.Close() 52 | } 53 | 54 | suite.NoError(err) 55 | suite.Equal(http.StatusOK, resp.StatusCode) 56 | } 57 | 58 | func TestSocks5(t *testing.T) { 59 | t.Parallel() 60 | suite.Run(t, &Socks5TestSuite{}) 61 | } 62 | -------------------------------------------------------------------------------- /antireplay/stable_bloom_filter.go: -------------------------------------------------------------------------------- 1 | package antireplay 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/9seconds/mtg/v2/mtglib" 7 | "github.com/OneOfOne/xxhash" 8 | boom "github.com/tylertreat/BoomFilters" 9 | ) 10 | 11 | type stableBloomFilter struct { 12 | filter boom.StableBloomFilter 13 | mutex sync.Mutex 14 | } 15 | 16 | func (s *stableBloomFilter) SeenBefore(digest []byte) bool { 17 | s.mutex.Lock() 18 | defer s.mutex.Unlock() 19 | 20 | return s.filter.TestAndAdd(digest) 21 | } 22 | 23 | // NewStableBloomFilter returns an implementation of AntiReplayCache based on 24 | // stable bloom filter. 25 | // 26 | // http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf 27 | // 28 | // The basic idea of a stable bloom filter is quite simple: each time when you 29 | // set a new element, you randomly reset P elements. There is a hardcore math 30 | // which proves that if you choose this P correctly, you can maintain the same 31 | // error rate for a stream of elements. 32 | // 33 | // byteSize is the number of bytes you want to give to a bloom filter. 34 | // errorRate is desired false-positive error rate. If you want to use default 35 | // values, please pass 0 for byteSize and <0 for errorRate. 36 | func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCache { 37 | if byteSize == 0 { 38 | byteSize = DefaultStableBloomFilterMaxSize 39 | } 40 | 41 | if errorRate < 0 { 42 | errorRate = DefaultStableBloomFilterErrorRate 43 | } 44 | 45 | sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate) //nolint: gomnd 46 | sf.SetHash(xxhash.New64()) 47 | 48 | return &stableBloomFilter{ 49 | filter: *sf, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/utils/read_config_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/utils" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type ReadConfigTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func (suite *ReadConfigTestSuite) GetConfigPath(filename string) string { 16 | return filepath.Join("testdata", filename) 17 | } 18 | 19 | func (suite *ReadConfigTestSuite) TestReadMinimal() { 20 | conf, err := utils.ReadConfig(suite.GetConfigPath("minimal.toml")) 21 | suite.NoError(err) 22 | suite.NoError(conf.Validate()) 23 | suite.Equal("0.0.0.0:80", conf.BindTo.Get("")) 24 | suite.Equal("7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t", conf.Secret.Base64()) 25 | } 26 | 27 | func (suite *ReadConfigTestSuite) TestReadAbsentFile() { 28 | _, err := utils.ReadConfig(suite.GetConfigPath("unknown.file")) 29 | suite.Error(err) 30 | } 31 | 32 | func (suite *ReadConfigTestSuite) TestBrokenFile() { 33 | _, err := utils.ReadConfig(suite.GetConfigPath("broken.toml")) 34 | suite.Error(err) 35 | } 36 | 37 | func (suite *ReadConfigTestSuite) TestMissedBindTo() { 38 | _, err := utils.ReadConfig(suite.GetConfigPath("missed-bindto.toml")) 39 | suite.Error(err) 40 | } 41 | 42 | func (suite *ReadConfigTestSuite) TestMissedSecret() { 43 | _, err := utils.ReadConfig(suite.GetConfigPath("missed-secret.toml")) 44 | suite.Error(err) 45 | } 46 | 47 | func (suite *ReadConfigTestSuite) TestEmpty() { 48 | _, err := utils.ReadConfig(suite.GetConfigPath("empty.toml")) 49 | suite.Error(err) 50 | } 51 | 52 | func TestReadConfig(t *testing.T) { 53 | t.Parallel() 54 | suite.Run(t, &ReadConfigTestSuite{}) 55 | } 56 | -------------------------------------------------------------------------------- /internal/config/type_http_path_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type typeHTTPPathTestStruct struct { 13 | Value config.TypeHTTPPath `json:"value"` 14 | } 15 | 16 | type TypeHTTPPathTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func (suite *TypeHTTPPathTestSuite) TestUnmarshalOk() { 21 | testData := map[string]string{ 22 | "": "/", 23 | "/": "/", 24 | "/path": "/path", 25 | "path": "/path", 26 | } 27 | 28 | for k, v := range testData { 29 | value := v 30 | 31 | data, err := json.Marshal(map[string]string{ 32 | "value": k, 33 | }) 34 | suite.NoError(err) 35 | 36 | suite.T().Run(k, func(t *testing.T) { 37 | testStruct := &typeHTTPPathTestStruct{} 38 | assert.NoError(t, json.Unmarshal(data, testStruct)) 39 | assert.Equal(t, value, testStruct.Value.Get("")) 40 | }) 41 | } 42 | } 43 | 44 | func (suite *TypeHTTPPathTestSuite) TestMarshalOk() { 45 | value := typeHTTPPathTestStruct{ 46 | Value: config.TypeHTTPPath{ 47 | Value: "/path", 48 | }, 49 | } 50 | 51 | data, err := json.Marshal(value) 52 | suite.NoError(err) 53 | suite.JSONEq(`{"value": "/path"}`, string(data)) 54 | } 55 | 56 | func (suite *TypeHTTPPathTestSuite) TestGet() { 57 | value := config.TypeHTTPPath{} 58 | suite.Equal("/hello", value.Get("/hello")) 59 | 60 | suite.NoError(value.Set("/lalala")) 61 | suite.Equal("/lalala", value.Get("/hello")) 62 | } 63 | 64 | func TestTypeHTTPPath(t *testing.T) { 65 | t.Parallel() 66 | suite.Run(t, &TypeHTTPPathTestSuite{}) 67 | } 68 | -------------------------------------------------------------------------------- /internal/config/type_port_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type typePortTestStruct struct { 13 | Value config.TypePort `json:"value"` 14 | } 15 | 16 | type TypePortTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func (suite *TypePortTestSuite) TestUnmarshalFail() { 21 | testData := []string{ 22 | "", 23 | "port", 24 | "0", 25 | "-1", 26 | "1.5", 27 | "70000", 28 | } 29 | 30 | for _, v := range testData { 31 | data, err := json.Marshal(map[string]string{ 32 | "value": v, 33 | }) 34 | suite.NoError(err) 35 | 36 | suite.T().Run(v, func(t *testing.T) { 37 | assert.Error(t, json.Unmarshal(data, &typePortTestStruct{})) 38 | }) 39 | } 40 | } 41 | 42 | func (suite *TypePortTestSuite) TestUnmarshalOk() { 43 | testStruct := &typePortTestStruct{} 44 | suite.NoError(json.Unmarshal([]byte(`{"value": 5}`), testStruct)) 45 | suite.EqualValues(5, testStruct.Value.Value) 46 | } 47 | 48 | func (suite *TypePortTestSuite) TestMarshalOk() { 49 | testStruct := &typePortTestStruct{ 50 | Value: config.TypePort{ 51 | Value: 10, 52 | }, 53 | } 54 | 55 | data, err := json.Marshal(testStruct) 56 | suite.NoError(err) 57 | suite.JSONEq(`{"value":10}`, string(data)) 58 | } 59 | 60 | func (suite *TypePortTestSuite) TestGet() { 61 | value := config.TypePort{} 62 | suite.EqualValues(10, value.Get(10)) 63 | 64 | value.Value = 100 65 | suite.EqualValues(100, value.Get(10)) 66 | } 67 | 68 | func TestTypePort(t *testing.T) { 69 | t.Parallel() 70 | suite.Run(t, &TypePortTestSuite{}) 71 | } 72 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/server_handshake_fuzz_test.go: -------------------------------------------------------------------------------- 1 | package obfuscated2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | func FuzzServerSend(f *testing.F) { 11 | f.Add([]byte{1, 2, 3, 4, 5}) 12 | 13 | f.Fuzz(func(t *testing.T, data []byte) { 14 | handshakeData := NewServerHandshakeTestData(t) 15 | 16 | handshakeData.connMock. 17 | On("Write", mock.Anything). 18 | Return(len(data), nil). 19 | Once(). 20 | Run(func(args mock.Arguments) { 21 | message := make([]byte, len(data)) 22 | handshakeData.decryptor.XORKeyStream(message, args.Get(0).([]byte)) //nolint: forcetypeassert 23 | assert.Equal(t, message, data) 24 | }) 25 | 26 | n, err := handshakeData.proxyConn.Write(data) 27 | 28 | assert.EqualValues(t, len(data), n) 29 | assert.NoError(t, err) 30 | handshakeData.connMock.AssertExpectations(t) 31 | }) 32 | } 33 | 34 | func FuzzServerReceive(f *testing.F) { 35 | f.Add([]byte{1, 2, 3, 4, 5}) 36 | 37 | f.Fuzz(func(t *testing.T, data []byte) { 38 | handshakeData := NewServerHandshakeTestData(t) 39 | buffer := make([]byte, len(data)) 40 | 41 | handshakeData.connMock. 42 | On("Read", mock.Anything). 43 | Return(len(data), nil). 44 | Once(). 45 | Run(func(args mock.Arguments) { 46 | message := make([]byte, len(data)) 47 | handshakeData.encryptor.XORKeyStream(message, data) 48 | copy(args.Get(0).([]byte), message) //nolint: forcetypeassert 49 | }) 50 | 51 | n, err := handshakeData.proxyConn.Read(buffer) 52 | 53 | assert.EqualValues(t, len(data), n) 54 | assert.NoError(t, err) 55 | assert.Equal(t, data, buffer) 56 | handshakeData.connMock.AssertExpectations(t) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/conn.go: -------------------------------------------------------------------------------- 1 | package faketls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | 8 | "github.com/9seconds/mtg/v2/essentials" 9 | "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" 10 | ) 11 | 12 | type Conn struct { 13 | essentials.Conn 14 | 15 | readBuffer bytes.Buffer 16 | } 17 | 18 | func (c *Conn) Read(p []byte) (int, error) { 19 | if n, _ := c.readBuffer.Read(p); n > 0 { 20 | return n, nil 21 | } 22 | 23 | rec := record.AcquireRecord() 24 | defer record.ReleaseRecord(rec) 25 | 26 | for { 27 | if err := rec.Read(c.Conn); err != nil { 28 | return 0, err //nolint: wrapcheck 29 | } 30 | 31 | switch rec.Type { //nolint: exhaustive 32 | case record.TypeApplicationData: 33 | rec.Payload.WriteTo(&c.readBuffer) //nolint: errcheck 34 | 35 | return c.readBuffer.Read(p) //nolint: wrapcheck 36 | case record.TypeChangeCipherSpec: 37 | default: 38 | return 0, fmt.Errorf("unsupported record type %v", rec.Type) 39 | } 40 | } 41 | } 42 | 43 | func (c *Conn) Write(p []byte) (int, error) { 44 | rec := record.AcquireRecord() 45 | defer record.ReleaseRecord(rec) 46 | 47 | rec.Type = record.TypeApplicationData 48 | rec.Version = record.Version12 49 | 50 | sendBuffer := acquireBytesBuffer() 51 | defer releaseBytesBuffer(sendBuffer) 52 | 53 | lenP := len(p) 54 | 55 | for len(p) > 0 { 56 | chunkSize := rand.Intn(record.TLSMaxRecordSize) 57 | if chunkSize > len(p) || chunkSize == 0 { 58 | chunkSize = len(p) 59 | } 60 | 61 | rec.Payload.Reset() 62 | rec.Payload.Write(p[:chunkSize]) 63 | rec.Dump(sendBuffer) //nolint: errcheck 64 | 65 | p = p[chunkSize:] 66 | } 67 | 68 | if _, err := c.Conn.Write(sendBuffer.Bytes()); err != nil { 69 | return 0, err //nolint: wrapcheck 70 | } 71 | 72 | return lenP, nil 73 | } 74 | -------------------------------------------------------------------------------- /network/default.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/9seconds/mtg/v2/essentials" 10 | ) 11 | 12 | type defaultDialer struct { 13 | net.Dialer 14 | } 15 | 16 | func (d *defaultDialer) Dial(network, address string) (essentials.Conn, error) { 17 | return d.DialContext(context.Background(), network, address) 18 | } 19 | 20 | func (d *defaultDialer) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) { 21 | switch network { 22 | case "tcp", "tcp4", "tcp6": //nolint: goconst 23 | default: 24 | return nil, fmt.Errorf("unsupported network %s", network) 25 | } 26 | 27 | conn, err := d.Dialer.DialContext(ctx, network, address) 28 | if err != nil { 29 | return nil, fmt.Errorf("cannot dial to %s: %w", address, err) 30 | } 31 | 32 | // we do not need to call to end user. End users call us. 33 | if err := SetServerSocketOptions(conn, 0); err != nil { 34 | conn.Close() 35 | 36 | return nil, fmt.Errorf("cannot set socket options: %w", err) 37 | } 38 | 39 | return conn.(essentials.Conn), nil //nolint: forcetypeassert 40 | } 41 | 42 | // NewDefaultDialer build a new dialer which dials bypassing proxies 43 | // etc. 44 | // 45 | // The most default one you can imagine. But it has tunes TCP 46 | // connections and setups SO_REUSEPORT. 47 | // 48 | // bufferSize is deprecated and ignored. It is kept here for backward 49 | // compatibility. 50 | func NewDefaultDialer(timeout time.Duration, bufferSize int) (Dialer, error) { 51 | switch { 52 | case timeout < 0: 53 | return nil, fmt.Errorf("timeout %v should be positive number", timeout) 54 | case timeout == 0: 55 | timeout = DefaultTimeout 56 | } 57 | 58 | return &defaultDialer{ 59 | Dialer: net.Dialer{ 60 | Timeout: timeout, 61 | }, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /network/default_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/network" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type DefaultDialerTestSuite struct { 13 | suite.Suite 14 | HTTPServerTestSuite 15 | 16 | d network.Dialer 17 | } 18 | 19 | func (suite *DefaultDialerTestSuite) SetupSuite() { 20 | suite.HTTPServerTestSuite.SetupSuite() 21 | 22 | d, err := network.NewDefaultDialer(0, 0) 23 | suite.NoError(err) 24 | 25 | suite.d = d 26 | } 27 | 28 | func (suite *DefaultDialerTestSuite) TestNegativeTimeout() { 29 | _, err := network.NewDefaultDialer(-1, 0) 30 | suite.Error(err) 31 | } 32 | 33 | func (suite *DefaultDialerTestSuite) TestUnsupportedProtocol() { 34 | _, err := suite.d.DialContext(context.Background(), 35 | "udp", 36 | suite.HTTPServerAddress()) 37 | suite.Error(err) 38 | } 39 | 40 | func (suite *DefaultDialerTestSuite) TestCannotDial() { 41 | _, err := suite.d.DialContext(context.Background(), 42 | "tcp", 43 | suite.HTTPServerAddress()+suite.HTTPServerAddress()) 44 | suite.Error(err) 45 | } 46 | 47 | func (suite *DefaultDialerTestSuite) TestConnectOk() { 48 | conn, err := suite.d.DialContext(context.Background(), 49 | "tcp", 50 | suite.HTTPServerAddress()) 51 | suite.NoError(err) 52 | suite.NotNil(conn) 53 | 54 | conn.Close() 55 | } 56 | 57 | func (suite *DefaultDialerTestSuite) TestHTTPRequest() { 58 | httpClient := suite.MakeHTTPClient(suite.d) 59 | 60 | resp, err := httpClient.Get(suite.MakeURL("/get")) //nolint: noctx 61 | if err == nil { 62 | defer resp.Body.Close() 63 | } 64 | 65 | suite.NoError(err) 66 | suite.Equal(http.StatusOK, resp.StatusCode) 67 | } 68 | 69 | func TestDefaultDialer(t *testing.T) { 70 | t.Parallel() 71 | suite.Run(t, &DefaultDialerTestSuite{}) 72 | } 73 | -------------------------------------------------------------------------------- /internal/config/type_concurrency_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type typeConcurrencyTestStruct struct { 13 | Value config.TypeConcurrency `json:"value"` 14 | } 15 | 16 | type TypeConcurrencyTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func (suite *TypeConcurrencyTestSuite) TestUnmarshalFail() { 21 | testData := []string{ 22 | "-1", 23 | "0", 24 | "0.0", 25 | "1.0", 26 | "1.1", 27 | ".", 28 | "some_value", 29 | } 30 | 31 | for _, v := range testData { 32 | data, err := json.Marshal(map[string]string{ 33 | "value": v, 34 | }) 35 | suite.NoError(err) 36 | 37 | suite.T().Run(v, func(t *testing.T) { 38 | assert.Error(t, json.Unmarshal(data, &typeConcurrencyTestStruct{})) 39 | }) 40 | } 41 | } 42 | 43 | func (suite *TypeConcurrencyTestSuite) TestUnmarshalOk() { 44 | testStruct := &typeConcurrencyTestStruct{} 45 | 46 | suite.NoError(json.Unmarshal([]byte(`{"value": 1}`), testStruct)) 47 | suite.EqualValues(1, testStruct.Value.Get(2)) 48 | } 49 | 50 | func (suite *TypeConcurrencyTestSuite) TestMarshalOk() { 51 | testStruct := &typeConcurrencyTestStruct{ 52 | Value: config.TypeConcurrency{ 53 | Value: 2, 54 | }, 55 | } 56 | 57 | data, err := json.Marshal(testStruct) 58 | suite.NoError(err) 59 | suite.JSONEq(`{"value": 2}`, string(data)) 60 | } 61 | 62 | func (suite *TypeConcurrencyTestSuite) TestGet() { 63 | value := config.TypeConcurrency{} 64 | suite.EqualValues(1, value.Get(1)) 65 | 66 | value.Value = 3 67 | suite.EqualValues(3, value.Get(1)) 68 | } 69 | 70 | func TestTypeConcurrency(t *testing.T) { 71 | t.Parallel() 72 | suite.Run(t, &TypeConcurrencyTestSuite{}) 73 | } 74 | -------------------------------------------------------------------------------- /internal/config/type_blocklist_uri.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type TypeBlocklistURI struct { 11 | Value string 12 | } 13 | 14 | func (t *TypeBlocklistURI) Set(value string) error { 15 | if stat, err := os.Stat(value); err == nil || os.IsExist(err) { 16 | switch { 17 | case stat.IsDir(): 18 | return fmt.Errorf("value is correct filepath but directory") 19 | case stat.Mode().Perm()&0o400 == 0: 20 | return fmt.Errorf("value is correct filepath but not readable") 21 | } 22 | 23 | value, err = filepath.Abs(value) 24 | if err != nil { 25 | return fmt.Errorf( 26 | "value is correct filepath but cannot resolve absolute (%s): %w", 27 | value, err) 28 | } 29 | 30 | t.Value = value 31 | 32 | return nil 33 | } 34 | 35 | parsedURL, err := url.Parse(value) 36 | if err != nil { 37 | return fmt.Errorf("incorrect url (%s): %w", value, err) 38 | } 39 | 40 | switch parsedURL.Scheme { 41 | case "http", "https": 42 | default: 43 | return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value) 44 | } 45 | 46 | if parsedURL.Host == "" { 47 | return fmt.Errorf("incorrect url %s", value) 48 | } 49 | 50 | t.Value = parsedURL.String() 51 | 52 | return nil 53 | } 54 | 55 | func (t TypeBlocklistURI) Get(defaultValue string) string { 56 | if t.Value == "" { 57 | return defaultValue 58 | } 59 | 60 | return t.Value 61 | } 62 | 63 | func (t TypeBlocklistURI) IsRemote() bool { 64 | return !filepath.IsAbs(t.Value) 65 | } 66 | 67 | func (t *TypeBlocklistURI) UnmarshalText(data []byte) error { 68 | return t.Set(string(data)) 69 | } 70 | 71 | func (t TypeBlocklistURI) MarshalText() ([]byte, error) { 72 | return []byte(t.String()), nil 73 | } 74 | 75 | func (t TypeBlocklistURI) String() string { 76 | return t.Value 77 | } 78 | -------------------------------------------------------------------------------- /internal/config/type_metric_prefix_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type typeMetricPrefixTestStruct struct { 13 | Value config.TypeMetricPrefix `json:"value"` 14 | } 15 | 16 | type TypeMetricPrefixTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() { 21 | testData := []string{ 22 | "", 23 | "-", 24 | "hello/world", 25 | "lala*", 26 | "++sdf++", 27 | } 28 | 29 | for _, v := range testData { 30 | data, err := json.Marshal(map[string]string{ 31 | "value": v, 32 | }) 33 | suite.NoError(err) 34 | 35 | suite.T().Run(v, func(t *testing.T) { 36 | assert.Error(t, json.Unmarshal(data, &typeMetricPrefixTestStruct{})) 37 | }) 38 | } 39 | } 40 | 41 | func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() { 42 | testStruct := &typeMetricPrefixTestStruct{} 43 | suite.NoError(json.Unmarshal([]byte(`{"value": "mtg"}`), testStruct)) 44 | suite.Equal("mtg", testStruct.Value.Get("lalala")) 45 | } 46 | 47 | func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() { 48 | testStruct := &typeMetricPrefixTestStruct{ 49 | Value: config.TypeMetricPrefix{ 50 | Value: "mtg", 51 | }, 52 | } 53 | 54 | data, err := json.Marshal(testStruct) 55 | suite.NoError(err) 56 | suite.JSONEq(`{"value": "mtg"}`, string(data)) 57 | } 58 | 59 | func (suite *TypeMetricPrefixTestSuite) TestGet() { 60 | value := config.TypeMetricPrefix{} 61 | suite.Equal("lalala", value.Get("lalala")) 62 | 63 | value.Value = "mtg" 64 | suite.Equal("mtg", value.Get("lalala")) 65 | } 66 | 67 | func TestTypeMetricPrefix(t *testing.T) { 68 | t.Parallel() 69 | suite.Run(t, &TypeMetricPrefixTestSuite{}) 70 | } 71 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/init.go: -------------------------------------------------------------------------------- 1 | package faketls 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | ) 7 | 8 | const ( 9 | // RandomLen defines a size of the random digest in TLS Hellos. 10 | RandomLen = 32 11 | 12 | // ClientHelloRandomOffset is an offset in ClientHello record where 13 | // random digest is started. 14 | ClientHelloRandomOffset = 6 15 | 16 | // ClientHelloSessionIDOffset is an offset in ClientHello record where 17 | // SessionID is started. 18 | ClientHelloSessionIDOffset = ClientHelloRandomOffset + RandomLen 19 | 20 | // ClientHelloMinLen is a minimal possible length of 21 | // ClientHello record. 22 | ClientHelloMinLen = 6 23 | 24 | // WelcomePacketRandomOffset is an offset of random in ServerHello 25 | // packet (including record envelope). 26 | WelcomePacketRandomOffset = 11 27 | 28 | // HandshakeTypeClient is a value representing a client handshake. 29 | HandshakeTypeClient = 0x01 30 | 31 | // HandshakeTypeServer is a value representing a server handshake. 32 | HandshakeTypeServer = 0x02 33 | 34 | // ChangeCipherValue is a value representing a change cipher 35 | // specification record. 36 | ChangeCipherValue = 0x01 37 | 38 | // ExtensionSNI is a value for TLS extension 'SNI'. 39 | ExtensionSNI = 0x00 40 | ) 41 | 42 | var ( 43 | // ErrBadDigest is returned if given TLS Client Hello mismatches with a 44 | // derived one. 45 | ErrBadDigest = errors.New("bad digest") 46 | 47 | serverHelloSuffix = []byte{ 48 | 0x00, // no compression 49 | 0x00, 0x2e, // 46 bytes of data 50 | 0x00, 0x2b, // Extension - Supported Versions 51 | 0x00, 0x02, // 2 bytes are following 52 | 0x03, 0x04, // TLS 1.3 53 | 0x00, 0x33, // Extension - Key Share 54 | 0x00, 0x24, // 36 bytes 55 | 0x00, 0x1d, // x25519 curve 56 | 0x00, 0x20, // 32 bytes of key 57 | } 58 | clientHelloEmptyRandom = bytes.Repeat([]byte{0}, RandomLen) 59 | ) 60 | -------------------------------------------------------------------------------- /mtglib/stream_context.go: -------------------------------------------------------------------------------- 1 | package mtglib 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "net" 8 | "time" 9 | 10 | "github.com/9seconds/mtg/v2/essentials" 11 | ) 12 | 13 | type streamContext struct { 14 | ctx context.Context 15 | ctxCancel context.CancelFunc 16 | clientConn essentials.Conn 17 | telegramConn essentials.Conn 18 | streamID string 19 | dc int 20 | logger Logger 21 | } 22 | 23 | func (s *streamContext) Deadline() (time.Time, bool) { 24 | return s.ctx.Deadline() 25 | } 26 | 27 | func (s *streamContext) Done() <-chan struct{} { 28 | return s.ctx.Done() 29 | } 30 | 31 | func (s *streamContext) Err() error { 32 | return s.ctx.Err() //nolint: wrapcheck 33 | } 34 | 35 | func (s *streamContext) Value(key interface{}) interface{} { 36 | return s.ctx.Value(key) 37 | } 38 | 39 | func (s *streamContext) Close() { 40 | s.ctxCancel() 41 | 42 | if s.clientConn != nil { 43 | s.clientConn.Close() 44 | } 45 | 46 | if s.telegramConn != nil { 47 | s.telegramConn.Close() 48 | } 49 | } 50 | 51 | func (s *streamContext) ClientIP() net.IP { 52 | return s.clientConn.RemoteAddr().(*net.TCPAddr).IP //nolint: forcetypeassert 53 | } 54 | 55 | func newStreamContext(ctx context.Context, logger Logger, clientConn essentials.Conn) *streamContext { 56 | connIDBytes := make([]byte, ConnectionIDBytesLength) 57 | 58 | if _, err := rand.Read(connIDBytes); err != nil { 59 | panic(err) 60 | } 61 | 62 | ctx, cancel := context.WithCancel(ctx) 63 | streamCtx := &streamContext{ 64 | ctx: ctx, 65 | ctxCancel: cancel, 66 | clientConn: clientConn, 67 | streamID: base64.RawURLEncoding.EncodeToString(connIDBytes), 68 | } 69 | streamCtx.logger = logger. 70 | BindStr("stream-id", streamCtx.streamID). 71 | BindStr("client-ip", streamCtx.ClientIP().String()) 72 | 73 | return streamCtx 74 | } 75 | -------------------------------------------------------------------------------- /network/load_balanced_socks5.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/url" 8 | 9 | "github.com/9seconds/mtg/v2/essentials" 10 | ) 11 | 12 | type loadBalancedSocks5Dialer struct { 13 | dialers []Dialer 14 | } 15 | 16 | func (l loadBalancedSocks5Dialer) Dial(network, address string) (essentials.Conn, error) { 17 | return l.DialContext(context.Background(), network, address) 18 | } 19 | 20 | func (l loadBalancedSocks5Dialer) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) { 21 | length := len(l.dialers) 22 | start := rand.Intn(length) 23 | moved := false 24 | 25 | for i := start; i != start || !moved; i = (i + 1) % length { 26 | moved = true 27 | 28 | if conn, err := l.dialers[i].DialContext(ctx, network, address); err == nil { 29 | return conn, nil 30 | } 31 | } 32 | 33 | return nil, ErrCannotDialWithAllProxies 34 | } 35 | 36 | // NewLoadBalancedSocks5Dialer builds a new load balancing SOCKS5 dialer. 37 | // 38 | // The main difference from one which is made by NewSocks5Dialer is that we 39 | // actually have a list of these proxies. When dial is requested, any proxy is 40 | // picked and used. If proxy fails for some reason, we try another one. 41 | // 42 | // So, it is mostly useful if you have some routes with proxies which are not 43 | // always online or having buggy network. 44 | func NewLoadBalancedSocks5Dialer(baseDialer Dialer, proxyURLs []*url.URL) (Dialer, error) { 45 | dialers := make([]Dialer, 0, len(proxyURLs)) 46 | 47 | for _, u := range proxyURLs { 48 | dialer, err := NewSocks5Dialer(newProxyDialer(baseDialer, u), u) 49 | if err != nil { 50 | return nil, fmt.Errorf("cannot build dialer for %s: %w", u.String(), err) 51 | } 52 | 53 | dialers = append(dialers, dialer) 54 | } 55 | 56 | return loadBalancedSocks5Dialer{ 57 | dialers: dialers, 58 | }, nil 59 | } 60 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/server_handshake.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "crypto/cipher" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | type serverHandshakeFrame struct { 12 | handshakeFrame 13 | } 14 | 15 | func (s *serverHandshakeFrame) decryptor() cipher.Stream { 16 | invertedHandshake := s.invert() 17 | 18 | return makeAesCtr(invertedHandshake.key(), invertedHandshake.iv()) 19 | } 20 | 21 | func (s *serverHandshakeFrame) encryptor() cipher.Stream { 22 | return makeAesCtr(s.key(), s.iv()) 23 | } 24 | 25 | func ServerHandshake(writer io.Writer) (cipher.Stream, cipher.Stream, error) { 26 | handshake := generateServerHanshakeFrame() 27 | copyHandshake := handshake 28 | encryptor := handshake.encryptor() 29 | decryptor := handshake.decryptor() 30 | 31 | encryptor.XORKeyStream(handshake.data[:], handshake.data[:]) 32 | copy(handshake.key(), copyHandshake.key()) 33 | copy(handshake.iv(), copyHandshake.iv()) 34 | 35 | if _, err := writer.Write(handshake.data[:]); err != nil { 36 | return nil, nil, fmt.Errorf("cannot send a handshake frame to telegram: %w", err) 37 | } 38 | 39 | return encryptor, decryptor, nil 40 | } 41 | 42 | func generateServerHanshakeFrame() serverHandshakeFrame { 43 | frame := serverHandshakeFrame{} 44 | 45 | for { 46 | if _, err := rand.Read(frame.data[:]); err != nil { 47 | panic(err) 48 | } 49 | 50 | if frame.data[0] == 0xef { //nolint: gomnd // taken from tg sources 51 | continue 52 | } 53 | 54 | switch binary.LittleEndian.Uint32(frame.data[:4]) { 55 | case 0x44414548, 0x54534f50, 0x20544547, 0x4954504f, 0xeeeeeeee: //nolint: gomnd // taken from tg sources 56 | continue 57 | } 58 | 59 | if frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7] == 0 { 60 | continue 61 | } 62 | 63 | copy(frame.connectionType(), handshakeConnectionType) 64 | 65 | return frame 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/config/type_error_rate_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type typeErrorRateTestStruct struct { 13 | Value config.TypeErrorRate `json:"value"` 14 | } 15 | 16 | type TypeErrorRateTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() { 21 | testData := []string{ 22 | "", 23 | "1s", 24 | "1,", 25 | "1,2", 26 | ".", 27 | "3.4.5", 28 | "3.5.", 29 | ".3.5", 30 | "some word", 31 | "1e2", 32 | "-1.0", 33 | } 34 | 35 | for _, v := range testData { 36 | data, err := json.Marshal(map[string]string{ 37 | "value": v, 38 | }) 39 | suite.NoError(err) 40 | 41 | suite.T().Run(v, func(t *testing.T) { 42 | assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{})) 43 | }) 44 | } 45 | } 46 | 47 | func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() { 48 | data, err := json.Marshal(map[string]float64{ 49 | "value": 1.0, 50 | }) 51 | suite.NoError(err) 52 | 53 | testStruct := &typeErrorRateTestStruct{} 54 | suite.NoError(json.Unmarshal(data, testStruct)) 55 | suite.InEpsilon(1.0, testStruct.Value.Value, 1e-10) 56 | } 57 | 58 | func (suite *TypeErrorRateTestSuite) TestMarshalOk() { 59 | testStruct := typeErrorRateTestStruct{ 60 | Value: config.TypeErrorRate{ 61 | Value: 1.01, 62 | }, 63 | } 64 | 65 | encodedJSON, err := json.Marshal(testStruct) 66 | suite.NoError(err) 67 | suite.JSONEq(`{"value": 1.01}`, string(encodedJSON)) 68 | } 69 | 70 | func (suite *TypeErrorRateTestSuite) TestGet() { 71 | value := config.TypeErrorRate{} 72 | suite.InEpsilon(1.0, value.Get(1.0), 1e-10) 73 | 74 | value.Value = 5.0 75 | suite.InEpsilon(5.0, value.Get(1.0), 1e-10) 76 | } 77 | 78 | func TestTypeErrorRate(t *testing.T) { 79 | t.Parallel() 80 | suite.Run(t, &TypeErrorRateTestSuite{}) 81 | } 82 | -------------------------------------------------------------------------------- /mtglib/internal/telegram/telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/9seconds/mtg/v2/essentials" 9 | ) 10 | 11 | type Telegram struct { 12 | dialer Dialer 13 | preferIP preferIP 14 | pool addressPool 15 | } 16 | 17 | func (t Telegram) Dial(ctx context.Context, dc int) (essentials.Conn, error) { 18 | var addresses []tgAddr 19 | 20 | switch t.preferIP { 21 | case preferIPOnlyIPv4: 22 | addresses = t.pool.getV4(dc) 23 | case preferIPOnlyIPv6: 24 | addresses = t.pool.getV6(dc) 25 | case preferIPPreferIPv4: 26 | addresses = append(t.pool.getV4(dc), t.pool.getV6(dc)...) 27 | case preferIPPreferIPv6: 28 | addresses = append(t.pool.getV6(dc), t.pool.getV4(dc)...) 29 | } 30 | 31 | var conn essentials.Conn 32 | 33 | err := errNoAddresses 34 | 35 | for _, v := range addresses { 36 | conn, err = t.dialer.DialContext(ctx, v.network, v.address) 37 | if err == nil { 38 | return conn, nil 39 | } 40 | } 41 | 42 | return nil, fmt.Errorf("cannot dial to %d dc: %w", dc, err) 43 | } 44 | 45 | func (t Telegram) IsKnownDC(dc int) bool { 46 | return t.pool.isValidDC(dc) 47 | } 48 | 49 | func (t Telegram) GetFallbackDC() int { 50 | return t.pool.getRandomDC() 51 | } 52 | 53 | func New(dialer Dialer, ipPreference string, useTestDCs bool) (*Telegram, error) { 54 | var pref preferIP 55 | 56 | switch strings.ToLower(ipPreference) { 57 | case "prefer-ipv4": 58 | pref = preferIPPreferIPv4 59 | case "prefer-ipv6": 60 | pref = preferIPPreferIPv6 61 | case "only-ipv4": 62 | pref = preferIPOnlyIPv4 63 | case "only-ipv6": 64 | pref = preferIPOnlyIPv6 65 | default: 66 | return nil, fmt.Errorf("unknown ip preference %s", ipPreference) 67 | } 68 | 69 | pool := addressPool{ 70 | v4: productionV4Addresses, 71 | v6: productionV6Addresses, 72 | } 73 | if useTestDCs { 74 | pool.v4 = testV4Addresses 75 | pool.v6 = testV6Addresses 76 | } 77 | 78 | return &Telegram{ 79 | dialer: dialer, 80 | preferIP: pref, 81 | pool: pool, 82 | }, nil 83 | } 84 | -------------------------------------------------------------------------------- /mtglib/internal/relay/relay_test.go: -------------------------------------------------------------------------------- 1 | package relay_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/testlib" 9 | "github.com/9seconds/mtg/v2/mtglib/internal/relay" 10 | "github.com/stretchr/testify/mock" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type RelayTestSuite struct { 15 | suite.Suite 16 | 17 | loggerMock relay.Logger 18 | ctx context.Context 19 | ctxCancel context.CancelFunc 20 | telegramConnMock *testlib.EssentialsConnMock 21 | clientConnMock *testlib.EssentialsConnMock 22 | } 23 | 24 | func (suite *RelayTestSuite) SetupTest() { 25 | ctx, cancel := context.WithCancel(context.Background()) 26 | suite.ctx = ctx 27 | suite.ctxCancel = cancel 28 | suite.loggerMock = &loggerMock{} 29 | suite.telegramConnMock = &testlib.EssentialsConnMock{} 30 | suite.clientConnMock = &testlib.EssentialsConnMock{} 31 | } 32 | 33 | func (suite *RelayTestSuite) TearDownTest() { 34 | suite.ctxCancel() 35 | suite.telegramConnMock.AssertExpectations(suite.T()) 36 | suite.clientConnMock.AssertExpectations(suite.T()) 37 | } 38 | 39 | func (suite *RelayTestSuite) TestExit() { 40 | suite.telegramConnMock.On("Close").Return(nil) 41 | suite.telegramConnMock.On("CloseRead").Return(nil).Once() 42 | suite.telegramConnMock.On("CloseWrite").Return(nil).Once() 43 | suite.telegramConnMock.On("Read", mock.Anything).Return(10, io.EOF).Once() 44 | suite.telegramConnMock.On("Write", mock.Anything).Return(10, io.EOF).Maybe() 45 | 46 | suite.clientConnMock.On("Read", mock.Anything).Return(0, io.EOF).Once() 47 | suite.clientConnMock.On("Write", mock.Anything).Return(10, io.EOF).Maybe() 48 | suite.clientConnMock.On("Close").Return(nil) 49 | suite.clientConnMock.On("CloseRead").Return(nil).Once() 50 | suite.clientConnMock.On("CloseWrite").Return(nil).Once() 51 | 52 | relay.Relay(suite.ctx, suite.loggerMock, suite.telegramConnMock, suite.clientConnMock) 53 | } 54 | 55 | func TestRelay(t *testing.T) { 56 | t.Parallel() 57 | suite.Run(t, &RelayTestSuite{}) 58 | } 59 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/server_handshake_test.go: -------------------------------------------------------------------------------- 1 | package obfuscated2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/mock" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type ServerHandshakeTestSuite struct { 11 | suite.Suite 12 | 13 | data ServerHandshakeTestData 14 | } 15 | 16 | func (suite *ServerHandshakeTestSuite) SetupTest() { 17 | suite.data = NewServerHandshakeTestData(suite.T()) 18 | } 19 | 20 | func (suite *ServerHandshakeTestSuite) TearDownTest() { 21 | suite.data.connMock.AssertExpectations(suite.T()) 22 | } 23 | 24 | func (suite *ServerHandshakeTestSuite) TestSendToTelegram() { 25 | messageToTelegram := []byte{10, 11, 12, 13, 14, 'a'} 26 | 27 | suite.data.connMock. 28 | On("Write", mock.Anything). 29 | Return(len(messageToTelegram), nil). 30 | Once(). 31 | Run(func(args mock.Arguments) { 32 | message := make([]byte, len(messageToTelegram)) 33 | suite.data.decryptor.XORKeyStream(message, args.Get(0).([]byte)) //nolint: forcetypeassert 34 | suite.Equal(messageToTelegram, message) 35 | }) 36 | 37 | n, err := suite.data.proxyConn.Write(messageToTelegram) 38 | suite.EqualValues(len(messageToTelegram), n) 39 | suite.NoError(err) 40 | } 41 | 42 | func (suite *ServerHandshakeTestSuite) TestRecieveFromTelegram() { 43 | messageFromTelegram := []byte{10, 11, 12, 13, 14, 'a'} 44 | buffer := make([]byte, len(messageFromTelegram)) 45 | 46 | suite.data.connMock. 47 | On("Read", mock.Anything). 48 | Return(len(messageFromTelegram), nil). 49 | Once(). 50 | Run(func(args mock.Arguments) { 51 | message := make([]byte, len(messageFromTelegram)) 52 | suite.data.encryptor.XORKeyStream(message, messageFromTelegram) 53 | copy(args.Get(0).([]byte), message) //nolint: forcetypeassert 54 | }) 55 | 56 | n, err := suite.data.proxyConn.Read(buffer) 57 | suite.EqualValues(len(messageFromTelegram), n) 58 | suite.NoError(err) 59 | suite.Equal(messageFromTelegram, buffer) 60 | } 61 | 62 | func TestServerHandshake(t *testing.T) { 63 | t.Parallel() 64 | suite.Run(t, &ServerHandshakeTestSuite{}) 65 | } 66 | -------------------------------------------------------------------------------- /internal/config/type_bytes_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type typeBytesTestStruct struct { 13 | Value config.TypeBytes `json:"value"` 14 | } 15 | 16 | type TypeBytesTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func (suite *TypeBytesTestSuite) TestUnmarshalFail() { 21 | testData := []string{ 22 | "1m", 23 | "1", 24 | "-1kb", 25 | "-1kib", 26 | } 27 | 28 | for _, v := range testData { 29 | data, err := json.Marshal(map[string]string{ 30 | "value": v, 31 | }) 32 | suite.NoError(err) 33 | 34 | suite.T().Run(v, func(t *testing.T) { 35 | assert.Error(t, json.Unmarshal(data, &typeBytesTestStruct{})) 36 | }) 37 | } 38 | } 39 | 40 | func (suite *TypeBytesTestSuite) TestUnmarshalOk() { 41 | testData := map[string]uint{ 42 | "1b": 1, 43 | "1kb": 1024, 44 | "1kib": 1024, 45 | "2mb": 2 * 1024 * 1024, 46 | "2mib": 2 * 1024 * 1024, 47 | } 48 | 49 | for k, v := range testData { 50 | value := v 51 | 52 | data, err := json.Marshal(map[string]string{ 53 | "value": k, 54 | }) 55 | suite.NoError(err) 56 | 57 | suite.T().Run(k, func(t *testing.T) { 58 | testStruct := &typeBytesTestStruct{} 59 | 60 | assert.NoError(t, json.Unmarshal(data, testStruct)) 61 | assert.EqualValues(t, value, testStruct.Value.Get(0)) 62 | }) 63 | } 64 | } 65 | 66 | func (suite *TypeBytesTestSuite) TestMarshalOk() { 67 | value := typeBytesTestStruct{} 68 | suite.NoError(value.Value.Set("1kib")) 69 | 70 | data, err := json.Marshal(value) 71 | suite.NoError(err) 72 | suite.JSONEq(`{"value": "1kib"}`, string(data)) 73 | } 74 | 75 | func (suite *TypeBytesTestSuite) TestGet() { 76 | value := config.TypeBytes{} 77 | suite.EqualValues(1000, value.Get(1000)) 78 | 79 | suite.NoError(value.Set("1mib")) 80 | suite.EqualValues(1048576, value.Get(1000)) 81 | } 82 | 83 | func TestTypeBytes(t *testing.T) { 84 | t.Parallel() 85 | suite.Run(t, &TypeBytesTestSuite{}) 86 | } 87 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/init.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import "fmt" 4 | 5 | const TLSMaxRecordSize = 65535 // max uint16 6 | 7 | type Type uint8 8 | 9 | const ( 10 | // TypeChangeCipherSpec defines a byte value of the TLS record when a 11 | // peer wants to change a specifications of the chosen cipher. 12 | TypeChangeCipherSpec Type = 0x14 13 | 14 | // TypeHandshake defines a byte value of the TLS record when a peer 15 | // initiates a new TLS connection and wants to make a handshake 16 | // ceremony. 17 | TypeHandshake Type = 0x16 18 | 19 | // TypeApplicationData defines a byte value of the TLS record when a 20 | // peer sends an user data, not a control frames. 21 | TypeApplicationData Type = 0x17 22 | ) 23 | 24 | func (t Type) String() string { 25 | switch t { 26 | case TypeChangeCipherSpec: 27 | return "changeCipher(0x14)" 28 | case TypeHandshake: 29 | return "handshake(0x16)" 30 | case TypeApplicationData: 31 | return "applicationData(0x17)" 32 | } 33 | 34 | return fmt.Sprintf("unknown(%#x)", byte(t)) 35 | } 36 | 37 | func (t Type) Valid() error { 38 | switch t { 39 | case TypeChangeCipherSpec, TypeHandshake, TypeApplicationData: 40 | return nil 41 | } 42 | 43 | return fmt.Errorf("unknown type %#x", byte(t)) 44 | } 45 | 46 | type Version uint16 47 | 48 | const ( 49 | // Version10 defines a TLS1.0. 50 | Version10 Version = 769 // 0x03 0x01 51 | 52 | // Version11 defines a TLS1.1. 53 | Version11 Version = 770 // 0x03 0x02 54 | 55 | // Version12 defines a TLS1.2. 56 | Version12 Version = 771 // 0x03 0x03 57 | 58 | // Version13 defines a TLS1.3. 59 | Version13 Version = 772 // 0x03 0x04 60 | ) 61 | 62 | func (v Version) String() string { 63 | switch v { 64 | case Version10: 65 | return "tls1.0" 66 | case Version11: 67 | return "tls1.1" 68 | case Version12: 69 | return "tls1.2" 70 | case Version13: 71 | return "tls1.3" 72 | } 73 | 74 | return fmt.Sprintf("tls?(%d)", uint16(v)) 75 | } 76 | 77 | func (v Version) Valid() error { 78 | switch v { 79 | case Version10, Version11, Version12, Version13: 80 | return nil 81 | } 82 | 83 | return fmt.Errorf("unknown version %d", uint16(v)) 84 | } 85 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/handshake_frame_internal_test.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type HandshakeFrameTestSuite struct { 14 | suite.Suite 15 | } 16 | 17 | func (suite *HandshakeFrameTestSuite) Decode(value string) []byte { 18 | v, err := base64.RawStdEncoding.DecodeString(value) 19 | suite.NoError(err) 20 | 21 | return v 22 | } 23 | 24 | func (suite *HandshakeFrameTestSuite) Encode(value []byte) string { 25 | return base64.RawStdEncoding.EncodeToString(value) 26 | } 27 | 28 | func (suite *HandshakeFrameTestSuite) TestOk() { 29 | hf := handshakeFrame{} 30 | testFrame := suite.Decode( 31 | "L9TmCzzxl9bPKODBpZeVM/qqNUxQ/axxBup1S2ymbIfUd6f7YSyzzM9EmTFv2/XzGqJGEHuj2zofmUGBLghu5g") 32 | copy(hf.data[:], testFrame) 33 | 34 | suite.Equal("zyjgwaWXlTP6qjVMUP2scQbqdUtspmyH1Hen+2Ess8w", suite.Encode(hf.key())) 35 | suite.Equal("z0SZMW/b9fMaokYQe6PbOg", suite.Encode(hf.iv())) 36 | suite.Equal("H5lBgQ", suite.Encode(hf.connectionType())) 37 | suite.EqualValues(2094, hf.dc()) 38 | 39 | inverted := hf.invert() 40 | suite.Equal("OtujexBGohrz9dtvMZlEz8yzLGH7p3fUh2ymbEt16gY", suite.Encode(inverted.key())) 41 | suite.Equal("caz9UEw1qvozlZelweAozw", suite.Encode(inverted.iv())) 42 | suite.Equal("H5lBgQ", suite.Encode(inverted.connectionType())) 43 | suite.EqualValues(2094, inverted.dc()) 44 | } 45 | 46 | func (suite *HandshakeFrameTestSuite) TestDC() { 47 | testData := map[int16]int{ 48 | 1: 1, 49 | -1: 1, 50 | 0: DefaultDC, 51 | } 52 | 53 | for k, v := range testData { 54 | incoming := k 55 | expected := v 56 | 57 | suite.T().Run(strconv.Itoa(int(incoming)), func(t *testing.T) { 58 | frame := handshakeFrame{} 59 | 60 | rand.Read(frame.data[:]) //nolint: errcheck 61 | 62 | frame.data[handshakeFrameOffsetDC] = byte(incoming) 63 | frame.data[handshakeFrameOffsetDC+1] = byte(incoming >> 8) 64 | 65 | assert.Equal(t, expected, frame.dc()) 66 | }) 67 | } 68 | } 69 | 70 | func TestHandshakeFrame(t *testing.T) { 71 | t.Parallel() 72 | suite.Run(t, &HandshakeFrameTestSuite{}) 73 | } 74 | -------------------------------------------------------------------------------- /internal/config/type_hostport_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type typeHostPortTestStruct struct { 13 | Value config.TypeHostPort `json:"value"` 14 | } 15 | 16 | type TypeHostPortTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func (suite *TypeHostPortTestSuite) TestUnmarshalFail() { 21 | testData := []string{ 22 | ":", 23 | ":800", 24 | "127.0.0.1:8000000", 25 | "12...:80", 26 | "", 27 | "localhost", 28 | "google.com:", 29 | } 30 | 31 | for _, v := range testData { 32 | data, err := json.Marshal(map[string]string{ 33 | "value": v, 34 | }) 35 | suite.NoError(err) 36 | 37 | suite.T().Run(v, func(t *testing.T) { 38 | assert.Error(t, json.Unmarshal(data, &typeHostPortTestStruct{})) 39 | }) 40 | } 41 | } 42 | 43 | func (suite *TypeHostPortTestSuite) TestUnmarshalOk() { 44 | testData := []string{ 45 | "127.0.0.1:80", 46 | "10.0.0.10:6553", 47 | } 48 | 49 | for _, v := range testData { 50 | value := v 51 | 52 | data, err := json.Marshal(map[string]string{ 53 | "value": v, 54 | }) 55 | suite.NoError(err) 56 | 57 | suite.T().Run(v, func(t *testing.T) { 58 | testStruct := &typeHostPortTestStruct{} 59 | assert.NoError(t, json.Unmarshal(data, testStruct)) 60 | assert.Equal(t, value, testStruct.Value.Value) 61 | }) 62 | } 63 | } 64 | 65 | func (suite *TypeHostPortTestSuite) TestMarshalOk() { 66 | testStruct := typeHostPortTestStruct{ 67 | Value: config.TypeHostPort{ 68 | Value: "127.0.0.1:8000", 69 | }, 70 | } 71 | 72 | data, err := json.Marshal(testStruct) 73 | suite.NoError(err) 74 | suite.JSONEq(`{"value": "127.0.0.1:8000"}`, string(data)) 75 | } 76 | 77 | func (suite *TypeHostPortTestSuite) TestGet() { 78 | value := config.TypeHostPort{} 79 | suite.Equal("127.0.0.1:9000", value.Get("127.0.0.1:9000")) 80 | 81 | value.Value = "127.0.0.1:80" 82 | suite.Equal("127.0.0.1:80", value.Get("127.0.0.1:9000")) 83 | } 84 | 85 | func TestTypeHostPort(t *testing.T) { 86 | t.Parallel() 87 | suite.Run(t, &TypeHostPortTestSuite{}) 88 | } 89 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/welcome_test.go: -------------------------------------------------------------------------------- 1 | package faketls_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "math/rand" 8 | "testing" 9 | "time" 10 | 11 | "github.com/9seconds/mtg/v2/mtglib" 12 | "github.com/9seconds/mtg/v2/mtglib/internal/faketls" 13 | "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" 14 | "github.com/stretchr/testify/suite" 15 | ) 16 | 17 | type WelcomeTestSuite struct { 18 | suite.Suite 19 | 20 | h *faketls.ClientHello 21 | buf *bytes.Buffer 22 | secret mtglib.Secret 23 | } 24 | 25 | func (suite *WelcomeTestSuite) SetupTest() { 26 | suite.h = &faketls.ClientHello{ 27 | Time: time.Now(), 28 | Host: "google.com", 29 | CipherSuite: 4867, 30 | SessionID: make([]byte, 32), 31 | } 32 | 33 | _, err := rand.Read(suite.h.SessionID) 34 | suite.NoError(err) 35 | 36 | _, err = rand.Read(suite.h.Random[:]) 37 | suite.NoError(err) 38 | 39 | suite.buf = &bytes.Buffer{} 40 | 41 | suite.secret = mtglib.GenerateSecret("google.com") 42 | } 43 | 44 | func (suite *WelcomeTestSuite) TestOk() { 45 | suite.NoError(faketls.SendWelcomePacket(suite.buf, suite.secret.Key[:], *suite.h)) 46 | 47 | welcomePacket := []byte{} 48 | welcomePacket = append(welcomePacket, suite.buf.Bytes()...) 49 | 50 | rec := record.AcquireRecord() 51 | defer record.ReleaseRecord(rec) 52 | 53 | suite.NoError(rec.Read(suite.buf)) 54 | suite.Equal(record.TypeHandshake, rec.Type) 55 | suite.Equal(record.Version12, rec.Version) 56 | 57 | suite.NoError(rec.Read(suite.buf)) 58 | suite.Equal(record.TypeChangeCipherSpec, rec.Type) 59 | suite.Equal(record.Version12, rec.Version) 60 | 61 | suite.NoError(rec.Read(suite.buf)) 62 | suite.Equal(record.TypeApplicationData, rec.Type) 63 | suite.Equal(record.Version12, rec.Version) 64 | suite.Empty(suite.buf.Bytes()) 65 | 66 | random := make([]byte, 32) 67 | copy(random, welcomePacket[11:]) 68 | 69 | empty := make([]byte, 32) 70 | copy(welcomePacket[11:], empty) 71 | 72 | mac := hmac.New(sha256.New, suite.secret.Key[:]) 73 | mac.Write(suite.h.Random[:]) 74 | mac.Write(welcomePacket) 75 | 76 | suite.Equal(random, mac.Sum(nil)) 77 | } 78 | 79 | func TestWelcome(t *testing.T) { 80 | t.Parallel() 81 | suite.Run(t, &WelcomeTestSuite{}) 82 | } 83 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/record.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | type Record struct { 12 | Type Type 13 | Version Version 14 | Payload bytes.Buffer 15 | } 16 | 17 | func (r *Record) String() string { 18 | return fmt.Sprintf("", 19 | r.Type, 20 | r.Version, 21 | base64.StdEncoding.EncodeToString(r.Payload.Bytes())) 22 | } 23 | 24 | func (r *Record) Reset() { 25 | r.Payload.Reset() 26 | } 27 | 28 | func (r *Record) Read(reader io.Reader) error { 29 | r.Reset() 30 | 31 | buf := [2]byte{} 32 | 33 | if _, err := io.ReadFull(reader, buf[:1]); err != nil { 34 | return fmt.Errorf("cannot read type: %w", err) 35 | } 36 | 37 | r.Type = Type(buf[0]) 38 | if err := r.Type.Valid(); err != nil { 39 | return fmt.Errorf("invalid type: %w", err) 40 | } 41 | 42 | if _, err := io.ReadFull(reader, buf[:]); err != nil { 43 | return fmt.Errorf("cannot read version: %w", err) 44 | } 45 | 46 | r.Version = Version(binary.BigEndian.Uint16(buf[:])) 47 | if err := r.Version.Valid(); err != nil { 48 | return fmt.Errorf("invalid version: %w", err) 49 | } 50 | 51 | if _, err := io.ReadFull(reader, buf[:]); err != nil { 52 | return fmt.Errorf("cannot read payload length: %w", err) 53 | } 54 | 55 | length := int64(binary.BigEndian.Uint16(buf[:])) 56 | if _, err := io.CopyN(&r.Payload, reader, length); err != nil { 57 | return fmt.Errorf("cannot read payload: %w", err) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (r *Record) Dump(writer io.Writer) error { 64 | buf := [2]byte{byte(r.Type), 0} 65 | if _, err := writer.Write(buf[:1]); err != nil { 66 | return fmt.Errorf("cannot dump record type: %w", err) 67 | } 68 | 69 | binary.BigEndian.PutUint16(buf[:], uint16(r.Version)) 70 | 71 | if _, err := writer.Write(buf[:]); err != nil { 72 | return fmt.Errorf("cannot dump version: %w", err) 73 | } 74 | 75 | binary.BigEndian.PutUint16(buf[:], uint16(r.Payload.Len())) 76 | 77 | if _, err := writer.Write(buf[:]); err != nil { 78 | return fmt.Errorf("cannot dump payload length: %w", err) 79 | } 80 | 81 | if _, err := writer.Write(r.Payload.Bytes()); err != nil { 82 | return fmt.Errorf("cannot dump record: %w", err) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/init_test.go: -------------------------------------------------------------------------------- 1 | package record_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type TypeTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (suite *TypeTestSuite) TestChangeCipherSpec() { 15 | suite.Contains(record.TypeChangeCipherSpec.String(), "changeCipher") 16 | suite.Contains(record.TypeChangeCipherSpec.String(), "0x14") 17 | suite.NoError(record.TypeChangeCipherSpec.Valid()) 18 | } 19 | 20 | func (suite *TypeTestSuite) TestHandshake() { 21 | suite.Contains(record.TypeHandshake.String(), "handshake") 22 | suite.Contains(record.TypeHandshake.String(), "0x16") 23 | suite.NoError(record.TypeHandshake.Valid()) 24 | } 25 | 26 | func (suite *TypeTestSuite) TestApplicationData() { 27 | suite.Contains(record.TypeApplicationData.String(), "applicationData") 28 | suite.Contains(record.TypeApplicationData.String(), "0x17") 29 | suite.NoError(record.TypeApplicationData.Valid()) 30 | } 31 | 32 | func (suite *TypeTestSuite) TestUnknown() { 33 | value := record.Type(0x20) 34 | 35 | suite.Contains(value.String(), "unknown") 36 | suite.Contains(value.String(), "0x20") 37 | suite.Error(value.Valid()) 38 | } 39 | 40 | type VersionTestSuite struct { 41 | suite.Suite 42 | } 43 | 44 | func (suite *VersionTestSuite) Test10() { 45 | suite.Equal("tls1.0", record.Version10.String()) 46 | suite.NoError(record.Version10.Valid()) 47 | } 48 | 49 | func (suite *VersionTestSuite) Test11() { 50 | suite.Equal("tls1.1", record.Version11.String()) 51 | suite.NoError(record.Version11.Valid()) 52 | } 53 | 54 | func (suite *VersionTestSuite) Test12() { 55 | suite.Equal("tls1.2", record.Version12.String()) 56 | suite.NoError(record.Version12.Valid()) 57 | } 58 | 59 | func (suite *VersionTestSuite) Test13() { 60 | suite.Equal("tls1.3", record.Version13.String()) 61 | suite.NoError(record.Version13.Valid()) 62 | } 63 | 64 | func (suite *VersionTestSuite) TestUnknown() { 65 | value := record.Version(900) 66 | 67 | suite.Equal("tls?(900)", value.String()) 68 | suite.Error(value.Valid()) 69 | } 70 | 71 | func TestType(t *testing.T) { 72 | t.Parallel() 73 | suite.Run(t, &TypeTestSuite{}) 74 | } 75 | 76 | func TestVersion(t *testing.T) { 77 | t.Parallel() 78 | suite.Run(t, &VersionTestSuite{}) 79 | } 80 | -------------------------------------------------------------------------------- /ipblocklist/files/http_test.go: -------------------------------------------------------------------------------- 1 | package files_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/9seconds/mtg/v2/ipblocklist/files" 12 | "github.com/stretchr/testify/suite" 13 | ) 14 | 15 | type HTTPTestSuite struct { 16 | suite.Suite 17 | 18 | httpClient *http.Client 19 | httpServer *httptest.Server 20 | ctx context.Context 21 | ctxCancel context.CancelFunc 22 | } 23 | 24 | func (suite *HTTPTestSuite) makeFile(path string) (files.File, error) { 25 | return files.NewHTTP(suite.httpClient, suite.httpServer.URL+"/"+path) //nolint: wrapcheck 26 | } 27 | 28 | func (suite *HTTPTestSuite) SetupSuite() { 29 | mux := http.NewServeMux() 30 | 31 | mux.Handle("/", http.FileServer(http.Dir("testdata"))) 32 | 33 | suite.httpServer = httptest.NewServer(mux) 34 | suite.httpClient = suite.httpServer.Client() 35 | } 36 | 37 | func (suite *HTTPTestSuite) SetupTest() { 38 | suite.ctx, suite.ctxCancel = context.WithCancel(context.Background()) 39 | } 40 | 41 | func (suite *HTTPTestSuite) TearDownTest() { 42 | suite.ctxCancel() 43 | suite.httpServer.CloseClientConnections() 44 | } 45 | 46 | func (suite *HTTPTestSuite) TearDownSuite() { 47 | suite.httpServer.Close() 48 | } 49 | 50 | func (suite *HTTPTestSuite) TestBadURL() { 51 | _, err := files.NewHTTP(suite.httpClient, "sdfsdf") 52 | suite.Error(err) 53 | } 54 | 55 | func (suite *HTTPTestSuite) TestBadSchema() { 56 | _, err := files.NewHTTP(suite.httpClient, "gopher://lala") 57 | suite.Error(err) 58 | } 59 | 60 | func (suite *HTTPTestSuite) TestNilHTTPClient() { 61 | _, err := files.NewHTTP(nil, "") 62 | suite.Error(err) 63 | } 64 | 65 | func (suite *HTTPTestSuite) TestAbsentFile() { 66 | file, err := suite.makeFile("absent") 67 | suite.NoError(err) 68 | 69 | _, err = file.Open(suite.ctx) 70 | suite.Error(err) 71 | } 72 | 73 | func (suite *HTTPTestSuite) TestOk() { 74 | file, err := suite.makeFile("readable") 75 | suite.NoError(err) 76 | 77 | readCloser, err := file.Open(suite.ctx) 78 | suite.NoError(err) 79 | 80 | defer readCloser.Close() 81 | 82 | data, err := io.ReadAll(readCloser) 83 | suite.NoError(err) 84 | suite.Equal("Hooray!", strings.TrimSpace(string(data))) 85 | } 86 | 87 | func TestHTTP(t *testing.T) { 88 | t.Parallel() 89 | suite.Run(t, &HTTPTestSuite{}) 90 | } 91 | -------------------------------------------------------------------------------- /mtglib/internal/telegram/init.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/9seconds/mtg/v2/essentials" 8 | ) 9 | 10 | var errNoAddresses = errors.New("no addresses") 11 | 12 | type preferIP uint8 13 | 14 | const ( 15 | preferIPOnlyIPv4 preferIP = iota 16 | preferIPOnlyIPv6 17 | preferIPPreferIPv4 18 | preferIPPreferIPv6 19 | ) 20 | 21 | type tgAddr struct { 22 | network string 23 | address string 24 | } 25 | 26 | // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 27 | var ( 28 | productionV4Addresses = [][]tgAddr{ 29 | { // dc1 30 | {network: "tcp4", address: "149.154.175.50:443"}, 31 | }, 32 | { // dc2 33 | {network: "tcp4", address: "149.154.167.51:443"}, 34 | {network: "tcp4", address: "95.161.76.100:443"}, 35 | }, 36 | { // dc3 37 | {network: "tcp4", address: "149.154.175.100:443"}, 38 | }, 39 | { // dc4 40 | {network: "tcp4", address: "149.154.167.91:443"}, 41 | }, 42 | { // dc5 43 | {network: "tcp4", address: "149.154.171.5:443"}, 44 | }, 45 | } 46 | productionV6Addresses = [][]tgAddr{ 47 | { // dc1 48 | {network: "tcp6", address: "[2001:b28:f23d:f001::a]:443"}, 49 | }, 50 | { // dc2 51 | {network: "tcp6", address: "[2001:67c:04e8:f002::a]:443"}, 52 | }, 53 | { // dc3 54 | {network: "tcp6", address: "[2001:b28:f23d:f003::a]:443"}, 55 | }, 56 | { // dc4 57 | {network: "tcp6", address: "[2001:67c:04e8:f004::a]:443"}, 58 | }, 59 | { // dc5 60 | {network: "tcp6", address: "[2001:b28:f23f:f005::a]:443"}, 61 | }, 62 | } 63 | 64 | testV4Addresses = [][]tgAddr{ 65 | { // dc1 66 | {network: "tcp4", address: "149.154.175.10:443"}, 67 | }, 68 | { // dc2 69 | {network: "tcp4", address: "149.154.167.40:443"}, 70 | }, 71 | { // dc3 72 | {network: "tcp4", address: "149.154.175.117:443"}, 73 | }, 74 | } 75 | testV6Addresses = [][]tgAddr{ 76 | { // dc1 77 | {network: "tcp6", address: "[2001:b28:f23d:f001::e]:443"}, 78 | }, 79 | { // dc2 80 | {network: "tcp6", address: "[2001:67c:04e8:f002::e]:443"}, 81 | }, 82 | { // dc3 83 | {network: "tcp6", address: "[2001:b28:f23d:f003::e]:443"}, 84 | }, 85 | } 86 | ) 87 | 88 | type Dialer interface { 89 | DialContext(ctx context.Context, network, address string) (essentials.Conn, error) 90 | } 91 | -------------------------------------------------------------------------------- /network/dns_resolver.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | doh "github.com/babolivier/go-doh-client" 11 | ) 12 | 13 | const dnsResolverKeepTime = 10 * time.Minute 14 | 15 | type dnsResolverCacheEntry struct { 16 | ips []string 17 | createdAt time.Time 18 | } 19 | 20 | func (c dnsResolverCacheEntry) Ok() bool { 21 | return time.Since(c.createdAt) < dnsResolverKeepTime 22 | } 23 | 24 | type dnsResolver struct { 25 | resolver doh.Resolver 26 | cache map[string]dnsResolverCacheEntry 27 | cacheMutex sync.RWMutex 28 | } 29 | 30 | func (d *dnsResolver) LookupA(hostname string) []string { 31 | key := "\x00" + hostname 32 | 33 | d.cacheMutex.RLock() 34 | entry, ok := d.cache[key] 35 | d.cacheMutex.RUnlock() 36 | 37 | if ok && entry.Ok() { 38 | return entry.ips 39 | } 40 | 41 | var ips []string 42 | 43 | if recs, _, err := d.resolver.LookupA(hostname); err == nil { 44 | for _, v := range recs { 45 | ips = append(ips, v.IP4) 46 | } 47 | 48 | d.cacheMutex.Lock() 49 | d.cache[key] = dnsResolverCacheEntry{ 50 | ips: ips, 51 | createdAt: time.Now(), 52 | } 53 | d.cacheMutex.Unlock() 54 | } 55 | 56 | return ips 57 | } 58 | 59 | func (d *dnsResolver) LookupAAAA(hostname string) []string { 60 | key := "\x01" + hostname 61 | 62 | d.cacheMutex.RLock() 63 | entry, ok := d.cache[key] 64 | d.cacheMutex.RUnlock() 65 | 66 | if ok && entry.Ok() { 67 | return entry.ips 68 | } 69 | 70 | var ips []string 71 | 72 | if recs, _, err := d.resolver.LookupAAAA(hostname); err == nil { 73 | for _, v := range recs { 74 | ips = append(ips, v.IP6) 75 | } 76 | 77 | d.cacheMutex.Lock() 78 | d.cache[key] = dnsResolverCacheEntry{ 79 | ips: ips, 80 | createdAt: time.Now(), 81 | } 82 | d.cacheMutex.Unlock() 83 | } 84 | 85 | return ips 86 | } 87 | 88 | func newDNSResolver(hostname string, httpClient *http.Client) *dnsResolver { 89 | if net.ParseIP(hostname).To4() == nil { 90 | // the hostname is an IPv6 address 91 | hostname = fmt.Sprintf("[%s]", hostname) 92 | } 93 | 94 | return &dnsResolver{ 95 | resolver: doh.Resolver{ 96 | Host: hostname, 97 | Class: doh.IN, 98 | HTTPClient: httpClient, 99 | }, 100 | cache: map[string]dnsResolverCacheEntry{}, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /mtglib/stream_context_internal_test.go: -------------------------------------------------------------------------------- 1 | package mtglib 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/testlib" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type StreamContextTestSuite struct { 13 | suite.Suite 14 | 15 | connMock *testlib.EssentialsConnMock 16 | logger NoopLogger 17 | ctx *streamContext 18 | ctxCancel context.CancelFunc 19 | } 20 | 21 | func (suite *StreamContextTestSuite) SetupSuite() { 22 | suite.logger = NoopLogger{} 23 | } 24 | 25 | func (suite *StreamContextTestSuite) SetupTest() { 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | ctx = context.WithValue(ctx, "key", "value") //nolint: golint, staticcheck 28 | 29 | suite.ctxCancel = cancel 30 | suite.connMock = &testlib.EssentialsConnMock{} 31 | 32 | addr := &net.TCPAddr{ 33 | IP: net.ParseIP("10.0.0.10"), 34 | Port: 6676, 35 | } 36 | suite.connMock.On("RemoteAddr").Return(addr) 37 | 38 | suite.ctx = newStreamContext(ctx, suite.logger, suite.connMock) 39 | } 40 | 41 | func (suite *StreamContextTestSuite) TearDownTest() { 42 | suite.ctxCancel() 43 | suite.connMock.AssertExpectations(suite.T()) 44 | } 45 | 46 | func (suite *StreamContextTestSuite) TestContextInterface() { 47 | _, ok := suite.ctx.Deadline() 48 | suite.False(ok) 49 | 50 | select { 51 | case <-suite.ctx.Done(): 52 | suite.FailNow("unexpectedly done") 53 | default: 54 | } 55 | 56 | suite.NoError(suite.ctx.Err()) 57 | suite.Equal("value", suite.ctx.Value("key")) 58 | 59 | suite.ctxCancel() 60 | 61 | select { 62 | case <-suite.ctx.Done(): 63 | suite.Error(suite.ctx.Err()) 64 | default: 65 | suite.FailNow("unexpectedly not done") 66 | } 67 | } 68 | 69 | func (suite *StreamContextTestSuite) TestClientIP() { 70 | suite.Equal("10.0.0.10", suite.ctx.ClientIP().String()) 71 | } 72 | 73 | func (suite *StreamContextTestSuite) TestClose() { 74 | suite.connMock.On("Close").Once().Return(nil) 75 | 76 | tgConnMock := &testlib.EssentialsConnMock{} 77 | tgConnMock.On("Close").Once().Return(nil) 78 | 79 | suite.ctx.telegramConn = tgConnMock 80 | suite.ctx.Close() 81 | 82 | select { 83 | case <-suite.ctx.Done(): 84 | suite.Error(suite.ctx.Err()) 85 | default: 86 | suite.FailNow("unexpectedly not done") 87 | } 88 | 89 | tgConnMock.AssertExpectations(suite.T()) 90 | } 91 | 92 | func TestStreamContext(t *testing.T) { 93 | t.Parallel() 94 | suite.Run(t, &StreamContextTestSuite{}) 95 | } 96 | -------------------------------------------------------------------------------- /network/network_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/9seconds/mtg/v2/network" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type NetworkTestSuite struct { 15 | suite.Suite 16 | HTTPServerTestSuite 17 | 18 | dialer network.Dialer 19 | } 20 | 21 | func (suite *NetworkTestSuite) SetupTest() { 22 | dialer, err := network.NewDefaultDialer(0, 0) 23 | suite.NoError(err) 24 | 25 | suite.dialer = dialer 26 | } 27 | 28 | func (suite *NetworkTestSuite) TestLocalHTTPRequest() { 29 | ntw, err := network.NewNetwork(suite.dialer, "itsme", "1.1.1.1", 0) 30 | suite.NoError(err) 31 | 32 | client := ntw.MakeHTTPClient(nil) 33 | 34 | resp, err := client.Get(suite.httpServer.URL + "/headers") //nolint: noctx 35 | suite.NoError(err) 36 | 37 | defer resp.Body.Close() 38 | 39 | data, err := io.ReadAll(resp.Body) 40 | suite.NoError(err) 41 | suite.Equal(http.StatusOK, resp.StatusCode) 42 | 43 | jsonStruct := struct { 44 | Headers struct { 45 | UserAgent []string `json:"User-Agent"` //nolint: tagliatelle 46 | } `json:"headers"` 47 | }{} 48 | 49 | suite.NoError(json.Unmarshal(data, &jsonStruct)) 50 | suite.Equal([]string{"itsme"}, jsonStruct.Headers.UserAgent) 51 | } 52 | 53 | func (suite *NetworkTestSuite) TestRealHTTPRequest() { 54 | ntw, err := network.NewNetwork(suite.dialer, "itsme", "1.1.1.1", 0) 55 | suite.NoError(err) 56 | 57 | client := ntw.MakeHTTPClient(nil) 58 | 59 | resp, err := client.Get("https://httpbin.org/headers") //nolint: noctx 60 | suite.NoError(err) 61 | 62 | defer resp.Body.Close() 63 | 64 | data, err := io.ReadAll(resp.Body) 65 | suite.NoError(err) 66 | suite.Equal(http.StatusOK, resp.StatusCode) 67 | 68 | jsonStruct := struct { 69 | Headers struct { 70 | UserAgent string `json:"User-Agent"` //nolint: tagliatelle 71 | } `json:"headers"` 72 | }{} 73 | 74 | suite.NoError(json.Unmarshal(data, &jsonStruct)) 75 | suite.Equal("itsme", jsonStruct.Headers.UserAgent) 76 | } 77 | 78 | func (suite *NetworkTestSuite) TestIncorrectTimeout() { 79 | _, err := network.NewNetwork(suite.dialer, "itsme", "1.1.1.1", -time.Second) 80 | suite.Error(err) 81 | } 82 | 83 | func (suite *NetworkTestSuite) TestIncorrectDOHHostname() { 84 | _, err := network.NewNetwork(suite.dialer, "itsme", "doh.com", 0) 85 | suite.Error(err) 86 | } 87 | 88 | func TestNetwork(t *testing.T) { 89 | t.Parallel() 90 | suite.Run(t, &NetworkTestSuite{}) 91 | } 92 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/handshake_frame.go: -------------------------------------------------------------------------------- 1 | package obfuscated2 2 | 3 | const ( 4 | // DefaultDC defines a number of the default DC to use. This value used 5 | // only if a value from obfuscated2 handshake frame is 0 (default). 6 | DefaultDC = 2 7 | 8 | handshakeFrameLen = 64 9 | 10 | handshakeFrameLenKey = 32 11 | handshakeFrameLenIV = 16 12 | handshakeFrameLenConnectionType = 4 13 | 14 | handshakeFrameOffsetStart = 8 15 | handshakeFrameOffsetKey = handshakeFrameOffsetStart 16 | handshakeFrameOffsetIV = handshakeFrameOffsetKey + handshakeFrameLenKey 17 | handshakeFrameOffsetConnectionType = handshakeFrameOffsetIV + handshakeFrameLenIV 18 | handshakeFrameOffsetDC = handshakeFrameOffsetConnectionType + handshakeFrameLenConnectionType 19 | ) 20 | 21 | // Connection-Type: Secure. We support only fake tls. 22 | var handshakeConnectionType = []byte{0xdd, 0xdd, 0xdd, 0xdd} 23 | 24 | // A structure of obfuscated2 handshake frame is following: 25 | // 26 | // [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd]. 27 | // 28 | // - 8 bytes of noise 29 | // - 32 bytes of AES Key 30 | // - 16 bytes of AES IV 31 | // - 4 bytes of 'connection type' - this has some setting like a connection type 32 | // - 2 bytes of 'DC'. DC is little endian int16 33 | // - 2 bytes of noise 34 | type handshakeFrame struct { 35 | data [handshakeFrameLen]byte 36 | } 37 | 38 | func (h *handshakeFrame) dc() int { 39 | idx := int16(h.data[handshakeFrameOffsetDC]) | int16(h.data[handshakeFrameOffsetDC+1])<<8 //nolint: gomnd, lll // little endian for int16 is here 40 | 41 | switch { 42 | case idx > 0: 43 | return int(idx) 44 | case idx < 0: 45 | return -int(idx) 46 | default: 47 | return DefaultDC 48 | } 49 | } 50 | 51 | func (h *handshakeFrame) key() []byte { 52 | return h.data[handshakeFrameOffsetKey:handshakeFrameOffsetIV] 53 | } 54 | 55 | func (h *handshakeFrame) iv() []byte { 56 | return h.data[handshakeFrameOffsetIV:handshakeFrameOffsetConnectionType] 57 | } 58 | 59 | func (h *handshakeFrame) connectionType() []byte { 60 | return h.data[handshakeFrameOffsetConnectionType:handshakeFrameOffsetDC] 61 | } 62 | 63 | func (h *handshakeFrame) invert() handshakeFrame { 64 | copyFrame := *h 65 | 66 | for i := 0; i < handshakeFrameLenKey+handshakeFrameLenIV; i++ { 67 | copyFrame.data[handshakeFrameOffsetKey+i] = h.data[handshakeFrameOffsetConnectionType-1-i] 68 | } 69 | 70 | return copyFrame 71 | } 72 | -------------------------------------------------------------------------------- /events/noop_test.go: -------------------------------------------------------------------------------- 1 | package events_test 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/events" 9 | "github.com/9seconds/mtg/v2/mtglib" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type NoopTestSuite struct { 14 | suite.Suite 15 | 16 | testData map[string]mtglib.Event 17 | ctx context.Context 18 | } 19 | 20 | func (suite *NoopTestSuite) SetupSuite() { 21 | suite.testData = map[string]mtglib.Event{ 22 | "start": mtglib.NewEventStart("connID", net.ParseIP("127.0.0.1")), 23 | "connected-to-dc": mtglib.NewEventConnectedToDC("connID", net.ParseIP("127.1.0.1"), 2), 24 | "domain-fronting": mtglib.NewEventDomainFronting("connID"), 25 | "traffic": mtglib.NewEventTraffic("connID", 1000, true), 26 | "finish": mtglib.NewEventFinish("connID"), 27 | "concurrency-limited": mtglib.NewEventConcurrencyLimited(), 28 | "ip-blacklisted": mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")), 29 | "replay-attack": mtglib.NewEventReplayAttack("connID"), 30 | "ip-list-size": mtglib.NewEventIPListSize(10, true), 31 | } 32 | suite.ctx = context.Background() 33 | } 34 | 35 | func (suite *NoopTestSuite) TestStream() { 36 | stream := events.NewNoopStream() 37 | 38 | for name, v := range suite.testData { 39 | value := v 40 | 41 | suite.T().Run(name, func(t *testing.T) { 42 | stream.Send(suite.ctx, value) 43 | }) 44 | } 45 | } 46 | 47 | func (suite *NoopTestSuite) TestObserver() { 48 | observer := events.NewNoopObserver() 49 | 50 | for name, v := range suite.testData { 51 | value := v 52 | 53 | suite.T().Run(name, func(t *testing.T) { 54 | switch typedEvt := value.(type) { 55 | case mtglib.EventStart: 56 | observer.EventStart(typedEvt) 57 | case mtglib.EventConnectedToDC: 58 | observer.EventConnectedToDC(typedEvt) 59 | case mtglib.EventDomainFronting: 60 | observer.EventDomainFronting(typedEvt) 61 | case mtglib.EventFinish: 62 | observer.EventFinish(typedEvt) 63 | case mtglib.EventConcurrencyLimited: 64 | observer.EventConcurrencyLimited(typedEvt) 65 | case mtglib.EventIPBlocklisted: 66 | observer.EventIPBlocklisted(typedEvt) 67 | case mtglib.EventReplayAttack: 68 | observer.EventReplayAttack(typedEvt) 69 | case mtglib.EventIPListSize: 70 | observer.EventIPListSize(typedEvt) 71 | } 72 | }) 73 | } 74 | 75 | observer.Shutdown() 76 | } 77 | 78 | func TestNoop(t *testing.T) { 79 | t.Parallel() 80 | suite.Run(t, &NoopTestSuite{}) 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/9seconds/mtg/v2 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/OneOfOne/xxhash v1.2.8 7 | github.com/alecthomas/kong v0.6.1 8 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 9 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 10 | github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 11 | github.com/d4l3k/messagediff v1.2.1 // indirect 12 | github.com/golang/protobuf v1.5.2 // indirect 13 | github.com/gotd/td v0.34.0 14 | github.com/jarcoal/httpmock v1.0.8 15 | github.com/mccutchen/go-httpbin v1.1.1 16 | github.com/panjf2000/ants/v2 v2.5.0 17 | github.com/pelletier/go-toml v1.9.5 18 | github.com/prometheus/client_golang v1.13.0 19 | github.com/prometheus/common v0.37.0 // indirect 20 | github.com/prometheus/procfs v0.8.0 // indirect 21 | github.com/rs/zerolog v1.27.0 22 | github.com/smira/go-statsd v1.3.2 23 | github.com/stretchr/objx v0.3.0 // indirect 24 | github.com/stretchr/testify v1.7.2 25 | github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 26 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa 27 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 28 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 29 | google.golang.org/protobuf v1.28.1 // indirect 30 | ) 31 | 32 | require ( 33 | github.com/txthinking/socks5 v0.0.0-20220615051428-39268faee3e6 34 | github.com/yl2chen/cidranger v1.0.2 35 | ) 36 | 37 | require ( 38 | github.com/beorn7/perks v1.0.1 // indirect 39 | github.com/cenkalti/backoff/v4 v4.1.0 // indirect 40 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 41 | github.com/davecgh/go-spew v1.1.1 // indirect 42 | github.com/gotd/ige v0.1.5 // indirect 43 | github.com/gotd/xor v0.1.1 // indirect 44 | github.com/mattn/go-colorable v0.1.12 // indirect 45 | github.com/mattn/go-isatty v0.0.14 // indirect 46 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 47 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 48 | github.com/pmezard/go-difflib v1.0.0 // indirect 49 | github.com/prometheus/client_model v0.2.0 // indirect 50 | github.com/txthinking/runnergroup v0.0.0-20220212043759-8da8edb7dae8 // indirect 51 | github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect 52 | go.uber.org/atomic v1.7.0 // indirect 53 | go.uber.org/multierr v1.6.0 // indirect 54 | go.uber.org/zap v1.16.0 // indirect 55 | golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect 56 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/welcome.go: -------------------------------------------------------------------------------- 1 | package faketls 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "io" 9 | mrand "math/rand" 10 | 11 | "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" 12 | "golang.org/x/crypto/curve25519" 13 | ) 14 | 15 | func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello) error { 16 | buf := acquireBytesBuffer() 17 | defer releaseBytesBuffer(buf) 18 | 19 | rec := record.AcquireRecord() 20 | defer record.ReleaseRecord(rec) 21 | 22 | rec.Type = record.TypeHandshake 23 | rec.Version = record.Version12 24 | 25 | generateServerHello(&rec.Payload, clientHello) 26 | rec.Dump(buf) //nolint: errcheck 27 | rec.Reset() 28 | 29 | rec.Type = record.TypeChangeCipherSpec 30 | rec.Version = record.Version12 31 | rec.Payload.WriteByte(ChangeCipherValue) 32 | 33 | rec.Dump(buf) //nolint: errcheck 34 | rec.Reset() 35 | 36 | rec.Type = record.TypeApplicationData 37 | rec.Version = record.Version12 38 | 39 | if _, err := io.CopyN(&rec.Payload, rand.Reader, int64(1024+mrand.Intn(3092))); err != nil { //nolint: gomnd 40 | panic(err) 41 | } 42 | 43 | rec.Dump(buf) //nolint: errcheck 44 | 45 | packet := buf.Bytes() 46 | mac := hmac.New(sha256.New, secret) 47 | 48 | mac.Write(clientHello.Random[:]) 49 | mac.Write(packet) 50 | 51 | copy(packet[WelcomePacketRandomOffset:], mac.Sum(nil)) 52 | 53 | if _, err := writer.Write(packet); err != nil { 54 | return err //nolint: wrapcheck 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func generateServerHello(writer io.Writer, clientHello ClientHello) { 61 | bodyBuf := acquireBytesBuffer() 62 | defer releaseBytesBuffer(bodyBuf) 63 | 64 | sliceBuf := [2]byte{} 65 | digest := [RandomLen]byte{} 66 | 67 | binary.BigEndian.PutUint16(sliceBuf[:], uint16(record.Version12)) 68 | bodyBuf.Write(sliceBuf[:]) 69 | bodyBuf.Write(digest[:]) 70 | bodyBuf.WriteByte(byte(len(clientHello.SessionID))) 71 | bodyBuf.Write(clientHello.SessionID) 72 | 73 | binary.BigEndian.PutUint16(sliceBuf[:], clientHello.CipherSuite) 74 | bodyBuf.Write(sliceBuf[:]) 75 | bodyBuf.Write(serverHelloSuffix) 76 | 77 | scalar := [32]byte{} 78 | 79 | if _, err := rand.Read(scalar[:]); err != nil { 80 | panic(err) 81 | } 82 | 83 | curve, _ := curve25519.X25519(scalar[:], curve25519.Basepoint) 84 | bodyBuf.Write(curve) 85 | 86 | header := [4]byte{0, 0, 0, 0} 87 | binary.BigEndian.PutUint32(header[:], uint32(bodyBuf.Len())) 88 | header[0] = HandshakeTypeServer 89 | 90 | writer.Write(header[:]) //nolint: errcheck 91 | bodyBuf.WriteTo(writer) //nolint: errcheck 92 | } 93 | -------------------------------------------------------------------------------- /internal/config/type_bool_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/9seconds/mtg/v2/internal/config" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type typeBoolTestStruct struct { 15 | Value config.TypeBool `json:"value"` 16 | } 17 | 18 | type TypeBoolTestSuite struct { 19 | suite.Suite 20 | } 21 | 22 | func (suite *TypeBoolTestSuite) TestUnmarshalFail() { 23 | testData := []interface{}{ 24 | "", 25 | "np", 26 | "нет", 27 | int(10), 28 | []int{}, 29 | } 30 | 31 | for _, v := range testData { 32 | data, err := json.Marshal(map[string]interface{}{ 33 | "value": v, 34 | }) 35 | suite.NoError(err) 36 | 37 | suite.T().Run(fmt.Sprintf("%v", v), func(t *testing.T) { 38 | assert.Error(t, json.Unmarshal(data, &typeBoolTestStruct{})) 39 | }) 40 | } 41 | } 42 | 43 | func (suite *TypeBoolTestSuite) TestUnmarshalOk() { 44 | testData := []bool{ 45 | true, 46 | false, 47 | } 48 | 49 | for _, v := range testData { 50 | value := v 51 | 52 | data, err := json.Marshal(map[string]bool{ 53 | "value": v, 54 | }) 55 | suite.NoError(err) 56 | 57 | suite.T().Run(strconv.FormatBool(v), func(t *testing.T) { 58 | testStruct := &typeBoolTestStruct{} 59 | assert.NoError(t, json.Unmarshal(data, testStruct)) 60 | 61 | if value { 62 | assert.True(t, testStruct.Value.Value) 63 | } else { 64 | assert.False(t, testStruct.Value.Value) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func (suite *TypeBoolTestSuite) TestMarshalOk() { 71 | for _, v := range []bool{true, false} { 72 | value := v 73 | 74 | suite.T().Run(strconv.FormatBool(v), func(t *testing.T) { 75 | testStruct := typeBoolTestStruct{ 76 | Value: config.TypeBool{ 77 | Value: value, 78 | }, 79 | } 80 | 81 | encodedJSON, err := json.Marshal(testStruct) 82 | assert.NoError(t, err) 83 | 84 | expectedJSON, err := json.Marshal(map[string]bool{ 85 | "value": value, 86 | }) 87 | assert.NoError(t, err) 88 | 89 | assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) 90 | }) 91 | } 92 | } 93 | 94 | func (suite *TypeBoolTestSuite) TestGet() { 95 | value := config.TypeBool{} 96 | suite.False(value.Get(false)) 97 | suite.True(value.Get(true)) 98 | 99 | value.Value = true 100 | suite.True(value.Get(false)) 101 | suite.True(value.Get(true)) 102 | } 103 | 104 | func TestTypeBool(t *testing.T) { 105 | t.Parallel() 106 | suite.Run(t, &TypeBoolTestSuite{}) 107 | } 108 | -------------------------------------------------------------------------------- /internal/config/type_proxy_url_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/config" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type typeProxyURLTestStruct struct { 14 | Value config.TypeProxyURL `json:"value"` 15 | } 16 | 17 | type ProxyURLTestSuite struct { 18 | suite.Suite 19 | } 20 | 21 | func (suite *ProxyURLTestSuite) TestUnmarshalFail() { 22 | testData := []string{ 23 | "", 24 | "socks5://", 25 | "://lala", 26 | "/path", 27 | } 28 | 29 | for _, v := range testData { 30 | data, err := json.Marshal(map[string]string{ 31 | "value": v, 32 | }) 33 | suite.NoError(err) 34 | 35 | suite.T().Run(v, func(t *testing.T) { 36 | assert.Error(t, json.Unmarshal(data, &typeProxyURLTestStruct{})) 37 | }) 38 | } 39 | } 40 | 41 | func (suite *ProxyURLTestSuite) TestUnmarshalOk() { 42 | testData := map[string]string{ 43 | "socks5://127.0.0.1/?open_threshold=1": "socks5://127.0.0.1:1080/?open_threshold=1", 44 | "socks5://127.0.0.1:80": "socks5://127.0.0.1:80", 45 | } 46 | 47 | for k, v := range testData { 48 | value := v 49 | 50 | data, err := json.Marshal(map[string]string{ 51 | "value": k, 52 | }) 53 | suite.NoError(err) 54 | 55 | suite.T().Run(k, func(t *testing.T) { 56 | testStruct := &typeProxyURLTestStruct{} 57 | assert.NoError(t, json.Unmarshal(data, testStruct)) 58 | 59 | parsed, _ := url.Parse(value) 60 | 61 | assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme) 62 | assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host) 63 | assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery) 64 | assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path) 65 | }) 66 | } 67 | } 68 | 69 | func (suite *ProxyURLTestSuite) TestMarshalOk() { 70 | parsed, _ := url.Parse("socks5://127.0.0.1:1080?open_threshold=1") 71 | testStruct := &typeProxyURLTestStruct{ 72 | Value: config.TypeProxyURL{ 73 | Value: parsed, 74 | }, 75 | } 76 | 77 | encodedJSON, err := json.Marshal(testStruct) 78 | suite.NoError(err) 79 | suite.JSONEq(`{"value": "socks5://127.0.0.1:1080?open_threshold=1"}`, 80 | string(encodedJSON)) 81 | } 82 | 83 | func (suite *ProxyURLTestSuite) TestGet() { 84 | emptyURL := &url.URL{} 85 | 86 | value := config.TypeProxyURL{} 87 | suite.Equal(emptyURL, value.Get(emptyURL)) 88 | 89 | value.Value = &url.URL{} 90 | suite.Equal(value.Value, value.Get(emptyURL)) 91 | } 92 | 93 | func TestTypeProxyURL(t *testing.T) { 94 | t.Parallel() 95 | suite.Run(t, &ProxyURLTestSuite{}) 96 | } 97 | -------------------------------------------------------------------------------- /internal/config/type_ip_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/config" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type typeIPTestStruct struct { 14 | Value config.TypeIP `json:"value"` 15 | } 16 | 17 | type TypeIPTestSuite struct { 18 | suite.Suite 19 | } 20 | 21 | func (suite *TypeIPTestSuite) TestUnmarshalFail() { 22 | testData := []string{ 23 | "", 24 | "....", 25 | "0...", 26 | "300.200.200.800", 27 | "[]", 28 | } 29 | 30 | for _, v := range testData { 31 | data, err := json.Marshal(map[string]string{ 32 | "value": v, 33 | }) 34 | suite.NoError(err) 35 | 36 | suite.T().Run(v, func(t *testing.T) { 37 | assert.Error(t, json.Unmarshal(data, &typeIPTestStruct{})) 38 | }) 39 | } 40 | } 41 | 42 | func (suite *TypeIPTestSuite) TestUnmarshalOk() { 43 | testData := map[string]string{ 44 | "2001:0db8:85a3:0000:0000:8a2e:0370:7334": "2001:db8:85a3::8a2e:370:7334", 45 | "127.0.0.1": "127.0.0.1", 46 | } 47 | 48 | for k, v := range testData { 49 | expected := v 50 | 51 | data, err := json.Marshal(map[string]string{ 52 | "value": k, 53 | }) 54 | suite.NoError(err) 55 | 56 | suite.T().Run(k, func(t *testing.T) { 57 | testStruct := &typeIPTestStruct{} 58 | assert.NoError(t, json.Unmarshal(data, testStruct)) 59 | assert.Equal(t, expected, testStruct.Value.Get(nil).String()) 60 | }) 61 | } 62 | } 63 | 64 | func (suite *TypeIPTestSuite) TestMarshalOk() { 65 | testData := []string{ 66 | "2001:db8:85a3::8a2e:370:7334", 67 | "127.0.0.1", 68 | } 69 | 70 | for _, v := range testData { 71 | value := v 72 | 73 | suite.T().Run(v, func(t *testing.T) { 74 | testStruct := &typeIPTestStruct{ 75 | Value: config.TypeIP{ 76 | Value: net.ParseIP(value), 77 | }, 78 | } 79 | 80 | encodedJSON, err := json.Marshal(testStruct) 81 | assert.NoError(t, err) 82 | 83 | expectedJSON, err := json.Marshal(map[string]string{ 84 | "value": value, 85 | }) 86 | assert.NoError(t, err) 87 | 88 | assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) 89 | }) 90 | } 91 | } 92 | 93 | func (suite *TypeIPTestSuite) TestGet() { 94 | value := config.TypeIP{} 95 | suite.Equal("127.0.0.1", value.Get(net.ParseIP("127.0.0.1")).String()) 96 | 97 | suite.NoError(value.Set("127.0.0.2")) 98 | suite.Equal("127.0.0.2", value.Get(net.ParseIP("127.0.0.1")).String()) 99 | } 100 | 101 | func TestTypeIP(t *testing.T) { 102 | t.Parallel() 103 | suite.Run(t, &TypeIPTestSuite{}) 104 | } 105 | -------------------------------------------------------------------------------- /network/init_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/9seconds/mtg/v2/essentials" 12 | "github.com/9seconds/mtg/v2/network" 13 | socks5 "github.com/armon/go-socks5" 14 | "github.com/mccutchen/go-httpbin/httpbin" 15 | "github.com/stretchr/testify/mock" 16 | ) 17 | 18 | type DialerMock struct { 19 | mock.Mock 20 | } 21 | 22 | func (d *DialerMock) Dial(network, address string) (essentials.Conn, error) { 23 | args := d.Called(network, address) 24 | 25 | return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert 26 | } 27 | 28 | func (d *DialerMock) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) { 29 | args := d.Called(ctx, network, address) 30 | 31 | return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert 32 | } 33 | 34 | type HTTPServerTestSuite struct { 35 | httpServer *httptest.Server 36 | } 37 | 38 | func (suite *HTTPServerTestSuite) SetupSuite() { 39 | suite.httpServer = httptest.NewServer(httpbin.NewHTTPBin().Handler()) 40 | } 41 | 42 | func (suite *HTTPServerTestSuite) TearDownSuite() { 43 | suite.httpServer.Close() 44 | } 45 | 46 | func (suite *HTTPServerTestSuite) HTTPServerAddress() string { 47 | return strings.TrimPrefix(suite.httpServer.URL, "http://") 48 | } 49 | 50 | func (suite *HTTPServerTestSuite) MakeURL(path string) string { 51 | return suite.httpServer.URL + path 52 | } 53 | 54 | func (suite *HTTPServerTestSuite) MakeHTTPClient(dialer network.Dialer) *http.Client { 55 | return &http.Client{ 56 | Transport: &http.Transport{ 57 | DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { 58 | return dialer.DialContext(ctx, network, address) //nolint: wrapcheck 59 | }, 60 | }, 61 | } 62 | } 63 | 64 | type Socks5ServerTestSuite struct { 65 | socks5Listener net.Listener 66 | socks5Server *socks5.Server 67 | } 68 | 69 | func (suite *Socks5ServerTestSuite) SetupSuite() { 70 | suite.socks5Listener, _ = net.Listen("tcp", "127.0.0.1:0") 71 | suite.socks5Server, _ = socks5.New(&socks5.Config{ 72 | Credentials: socks5.StaticCredentials{ 73 | "user": "password", 74 | }, 75 | }) 76 | 77 | go suite.socks5Server.Serve(suite.socks5Listener) //nolint: errcheck 78 | } 79 | 80 | func (suite *Socks5ServerTestSuite) TearDownSuite() { 81 | suite.socks5Listener.Close() 82 | } 83 | 84 | func (suite *Socks5ServerTestSuite) MakeSocks5URL(user, password string) *url.URL { 85 | return &url.URL{ 86 | Scheme: "socks5", 87 | User: url.UserPassword(user, password), 88 | Host: suite.socks5Listener.Addr().String(), 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /mtglib/internal/obfuscated2/client_handshake_test.go: -------------------------------------------------------------------------------- 1 | package obfuscated2_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/9seconds/mtg/v2/internal/testlib" 8 | "github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type ClientHandshakeTestSuite struct { 15 | suite.Suite 16 | SnapshotTestSuite 17 | } 18 | 19 | func (suite *ClientHandshakeTestSuite) SetupSuite() { 20 | suite.NoError(suite.IngestSnapshots(".", "client-handshake-snapshot-")) 21 | } 22 | 23 | func (suite *ClientHandshakeTestSuite) TestCannotRead() { 24 | buf := bytes.NewBuffer([]byte{1, 2, 3}) 25 | _, _, _, err := obfuscated2.ClientHandshake([]byte{1, 2, 3}, buf) //nolint: dogsled 26 | 27 | suite.Error(err) 28 | } 29 | 30 | func (suite *ClientHandshakeTestSuite) TestOk() { 31 | for nameV, snapshotV := range suite.snapshots { 32 | snapshot := snapshotV 33 | 34 | suite.T().Run(nameV, func(t *testing.T) { 35 | buf := bytes.NewBuffer(snapshot.Frame.data) 36 | 37 | dc, encryptor, decryptor, err := obfuscated2.ClientHandshake( 38 | snapshot.Secret.data, buf) 39 | assert.NoError(t, err) 40 | assert.EqualValues(t, snapshot.DC, dc) 41 | 42 | writeData := make([]byte, len(snapshot.Encrypted.Text.data)) 43 | readData := make([]byte, len(snapshot.Decrypted.Text.data)) 44 | 45 | connMock := &testlib.EssentialsConnMock{} 46 | connMock.On("Read", mock.Anything). 47 | Once(). 48 | Return(len(snapshot.Decrypted.Text.data), nil). 49 | Run(func(args mock.Arguments) { 50 | arr, ok := args.Get(0).([]byte) 51 | 52 | suite.True(ok) 53 | copy(arr, snapshot.Decrypted.Cipher.data) 54 | }) 55 | connMock.On("Write", mock.Anything). 56 | Once(). 57 | Return(len(snapshot.Encrypted.Text.data), nil). 58 | Run(func(args mock.Arguments) { 59 | arr, ok := args.Get(0).([]byte) 60 | 61 | suite.True(ok) 62 | copy(writeData, arr) 63 | }) 64 | 65 | conn := obfuscated2.Conn{ 66 | Conn: connMock, 67 | Encryptor: encryptor, 68 | Decryptor: decryptor, 69 | } 70 | 71 | n, err := conn.Read(readData) 72 | assert.Equal(t, len(readData), n) 73 | assert.NoError(t, err) 74 | assert.Equal(t, snapshot.Decrypted.Text.data, readData) 75 | 76 | n, err = conn.Write(snapshot.Encrypted.Text.data) 77 | assert.Equal(t, len(writeData), n) 78 | assert.NoError(t, err) 79 | assert.Equal(t, snapshot.Encrypted.Cipher.data, writeData) 80 | 81 | connMock.AssertExpectations(t) 82 | }) 83 | } 84 | } 85 | 86 | func TestClientHandshake(t *testing.T) { 87 | t.Parallel() 88 | suite.Run(t, &ClientHandshakeTestSuite{}) 89 | } 90 | -------------------------------------------------------------------------------- /network/load_balanced_socks5_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "testing" 10 | 11 | "github.com/9seconds/mtg/v2/network" 12 | "github.com/stretchr/testify/mock" 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | type LoadBalancedSocks5TestSuite struct { 17 | suite.Suite 18 | HTTPServerTestSuite 19 | Socks5ServerTestSuite 20 | 21 | httpClient *http.Client 22 | } 23 | 24 | func (suite *LoadBalancedSocks5TestSuite) SetupSuite() { 25 | suite.HTTPServerTestSuite.SetupSuite() 26 | suite.Socks5ServerTestSuite.SetupSuite() 27 | } 28 | 29 | func (suite *LoadBalancedSocks5TestSuite) SetupTest() { 30 | baseDialer, _ := network.NewDefaultDialer(0, 0) 31 | lbDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, []*url.URL{ 32 | suite.MakeSocks5URL("user", "password"), 33 | suite.MakeSocks5URL("user2", "password"), 34 | }) 35 | suite.NoError(err) 36 | 37 | suite.httpClient = suite.MakeHTTPClient(lbDialer) 38 | } 39 | 40 | func (suite *LoadBalancedSocks5TestSuite) TearDownSuite() { 41 | suite.Socks5ServerTestSuite.SetupSuite() 42 | suite.HTTPServerTestSuite.SetupSuite() 43 | } 44 | 45 | func (suite *LoadBalancedSocks5TestSuite) TestIncorrectURL() { 46 | _, err := network.NewLoadBalancedSocks5Dialer(&DialerMock{}, []*url.URL{ 47 | {Scheme: "http"}, 48 | }) 49 | suite.Error(err) 50 | } 51 | 52 | func (suite *LoadBalancedSocks5TestSuite) TestCannotDial() { 53 | baseDialer := &DialerMock{} 54 | baseDialer.On("DialContext", mock.Anything, "tcp", "127.0.0.1:1080"). 55 | Times(network.ProxyDialerOpenThreshold). 56 | Return(&net.TCPConn{}, io.EOF) 57 | baseDialer.On("DialContext", mock.Anything, "tcp", "127.0.0.2:1080"). 58 | Times(network.ProxyDialerOpenThreshold). 59 | Return(&net.TCPConn{}, io.EOF) 60 | 61 | lbDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, []*url.URL{ 62 | {Scheme: "socks5", User: url.UserPassword("user", "password"), Host: "127.0.0.1:1080"}, 63 | {Scheme: "socks5", User: url.UserPassword("user", "password"), Host: "127.0.0.2:1080"}, 64 | }) 65 | suite.NoError(err) 66 | 67 | for i := 0; i < network.ProxyDialerOpenThreshold*2; i++ { 68 | _, err = lbDialer.Dial("tcp", "127.1.1.1:80") 69 | suite.True(errors.Is(err, network.ErrCannotDialWithAllProxies)) 70 | } 71 | 72 | baseDialer.AssertExpectations(suite.T()) 73 | } 74 | 75 | func (suite *LoadBalancedSocks5TestSuite) TestDialOk() { 76 | resp, err := suite.httpClient.Get(suite.MakeURL("/get")) //nolint: noctx 77 | if err == nil { 78 | defer resp.Body.Close() 79 | } 80 | 81 | suite.NoError(err) 82 | suite.Equal(http.StatusOK, resp.StatusCode) 83 | } 84 | 85 | func TestLoadBalancedSocks5(t *testing.T) { 86 | t.Parallel() 87 | suite.Run(t, &LoadBalancedSocks5TestSuite{}) 88 | } 89 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/9seconds/mtg/v2/mtglib" 9 | ) 10 | 11 | type Optional struct { 12 | Enabled TypeBool `json:"enabled"` 13 | } 14 | 15 | type ListConfig struct { 16 | Optional 17 | 18 | DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"` 19 | URLs []TypeBlocklistURI `json:"urls"` 20 | UpdateEach TypeDuration `json:"updateEach"` 21 | } 22 | 23 | type Config struct { 24 | Debug TypeBool `json:"debug"` 25 | AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"` 26 | Secret mtglib.Secret `json:"secret"` 27 | BindTo TypeHostPort `json:"bindTo"` 28 | PreferIP TypePreferIP `json:"preferIp"` 29 | DomainFrontingPort TypePort `json:"domainFrontingPort"` 30 | TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` 31 | Concurrency TypeConcurrency `json:"concurrency"` 32 | Defense struct { 33 | AntiReplay struct { 34 | Optional 35 | 36 | MaxSize TypeBytes `json:"maxSize"` 37 | ErrorRate TypeErrorRate `json:"errorRate"` 38 | } `json:"antiReplay"` 39 | Blocklist ListConfig `json:"blocklist"` 40 | Allowlist ListConfig `json:"allowlist"` 41 | } `json:"defense"` 42 | Network struct { 43 | Timeout struct { 44 | TCP TypeDuration `json:"tcp"` 45 | HTTP TypeDuration `json:"http"` 46 | Idle TypeDuration `json:"idle"` 47 | } `json:"timeout"` 48 | DOHIP TypeIP `json:"dohIp"` 49 | Proxies []TypeProxyURL `json:"proxies"` 50 | } `json:"network"` 51 | Stats struct { 52 | StatsD struct { 53 | Optional 54 | 55 | Address TypeHostPort `json:"address"` 56 | MetricPrefix TypeMetricPrefix `json:"metricPrefix"` 57 | TagFormat TypeStatsdTagFormat `json:"tagFormat"` 58 | } `json:"statsd"` 59 | Prometheus struct { 60 | Optional 61 | 62 | BindTo TypeHostPort `json:"bindTo"` 63 | HTTPPath TypeHTTPPath `json:"httpPath"` 64 | MetricPrefix TypeMetricPrefix `json:"metricPrefix"` 65 | } `json:"prometheus"` 66 | } `json:"stats"` 67 | } 68 | 69 | func (c *Config) Validate() error { 70 | if !c.Secret.Valid() { 71 | return fmt.Errorf("invalid secret %s", c.Secret.String()) 72 | } 73 | 74 | if c.BindTo.Get("") == "" { 75 | return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String()) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (c *Config) String() string { 82 | buf := &bytes.Buffer{} 83 | encoder := json.NewEncoder(buf) 84 | 85 | encoder.SetEscapeHTML(false) 86 | 87 | if err := encoder.Encode(c); err != nil { 88 | panic(err) 89 | } 90 | 91 | return buf.String() 92 | } 93 | -------------------------------------------------------------------------------- /internal/config/type_duration_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/9seconds/mtg/v2/internal/config" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type typeDurationTestStruct struct { 14 | Value config.TypeDuration `json:"value"` 15 | } 16 | 17 | type TypeDurationTestSuite struct { 18 | suite.Suite 19 | } 20 | 21 | func (suite *TypeDurationTestSuite) TestUnmarshalFail() { 22 | testData := []string{ 23 | "-1s", 24 | "1 seconds ago", 25 | "1s ago", 26 | "", 27 | } 28 | 29 | for _, v := range testData { 30 | data, err := json.Marshal(map[string]string{ 31 | "value": v, 32 | }) 33 | suite.NoError(err) 34 | 35 | suite.T().Run(v, func(t *testing.T) { 36 | assert.Error(t, json.Unmarshal(data, &typeDurationTestStruct{})) 37 | }) 38 | } 39 | } 40 | 41 | func (suite *TypeDurationTestSuite) TestUnmarshalOk() { 42 | testData := map[string]time.Duration{ 43 | "1s": time.Second, 44 | "0": 0 * time.Second, 45 | "0s": 0 * time.Second, 46 | "1\tM": time.Minute, 47 | "1H": time.Hour, 48 | "1 h": time.Hour, 49 | } 50 | 51 | for k, v := range testData { 52 | value := v 53 | 54 | data, err := json.Marshal(map[string]string{ 55 | "value": k, 56 | }) 57 | suite.NoError(err) 58 | 59 | suite.T().Run(k, func(t *testing.T) { 60 | testStruct := &typeDurationTestStruct{} 61 | 62 | assert.NoError(t, json.Unmarshal(data, testStruct)) 63 | assert.Equal(t, value, testStruct.Value.Value) 64 | }) 65 | } 66 | } 67 | 68 | func (suite *TypeDurationTestSuite) TestMarshalOk() { 69 | testData := map[string]string{ 70 | "1s": "1s", 71 | "0": "", 72 | "0s": "", 73 | "0ms": "", 74 | "1 H": "1h0m0s", 75 | } 76 | 77 | for k, v := range testData { 78 | value := k 79 | expected := v 80 | 81 | suite.T().Run(value, func(t *testing.T) { 82 | testStruct := &typeDurationTestStruct{} 83 | 84 | assert.NoError(t, testStruct.Value.Set(value)) 85 | 86 | data, err := json.Marshal(testStruct) 87 | assert.NoError(t, err) 88 | 89 | expectedJSON, err := json.Marshal(map[string]string{ 90 | "value": expected, 91 | }) 92 | assert.NoError(t, err) 93 | 94 | assert.JSONEq(t, string(expectedJSON), string(data)) 95 | }) 96 | } 97 | } 98 | 99 | func (suite *TypeDurationTestSuite) TestGet() { 100 | value := config.TypeDuration{} 101 | suite.Equal(time.Second, value.Get(time.Second)) 102 | 103 | value.Value = 3 * time.Second 104 | suite.Equal(3*time.Second, value.Get(time.Hour)) 105 | } 106 | 107 | func TestTypeDuration(t *testing.T) { 108 | t.Parallel() 109 | suite.Run(t, &TypeDurationTestSuite{}) 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions 15 | # https://github.com/github/codeql-action/issues/572 16 | permissions: 17 | actions: read 18 | contents: read 19 | pull-requests: read 20 | security-events: write 21 | 22 | on: 23 | push: 24 | branches: 25 | - master 26 | - stable 27 | pull_request: 28 | # The branches below must be a subset of the branches above 29 | branches: [ master ] 30 | schedule: 31 | - cron: '24 20 * * 5' 32 | 33 | jobs: 34 | analyze: 35 | name: Analyze 36 | runs-on: ubuntu-latest 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'go' ] 42 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 43 | # Learn more: 44 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 45 | 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v2 49 | 50 | # Initializes the CodeQL tools for scanning. 51 | - name: Initialize CodeQL 52 | uses: github/codeql-action/init@v1 53 | with: 54 | languages: ${{ matrix.language }} 55 | # If you wish to specify custom queries, you can do so here or in a config file. 56 | # By default, queries listed here will override any specified in a config file. 57 | # Prefix the list here with "+" to use these queries and those in the config file. 58 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 59 | 60 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 61 | # If this step fails, then you should remove it and run the build manually (see below) 62 | - name: Autobuild 63 | uses: github/codeql-action/autobuild@v1 64 | 65 | # ℹ️ Command-line programs to run using the OS shell. 66 | # 📚 https://git.io/JvXDl 67 | 68 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 69 | # and modify them (or add more) to build your code if your project 70 | # uses a compiled language 71 | 72 | #- run: | 73 | # make bootstrap 74 | # make release 75 | 76 | - name: Perform CodeQL Analysis 77 | uses: github/codeql-action/analyze@v1 78 | -------------------------------------------------------------------------------- /mtglib/events_test.go: -------------------------------------------------------------------------------- 1 | package mtglib_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/9seconds/mtg/v2/mtglib" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type EventsTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func (suite *EventsTestSuite) TestEventStart() { 17 | evt := mtglib.NewEventStart("CONNID", net.ParseIP("10.0.0.10")) 18 | 19 | suite.Equal("CONNID", evt.StreamID()) 20 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 21 | } 22 | 23 | func (suite *EventsTestSuite) TestEventFinish() { 24 | evt := mtglib.NewEventFinish("CONNID") 25 | 26 | suite.Equal("CONNID", evt.StreamID()) 27 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 28 | } 29 | 30 | func (suite *EventsTestSuite) TestEventConnectedToDC() { 31 | evt := mtglib.NewEventConnectedToDC("CONNID", net.ParseIP("10.0.0.10"), 3) 32 | 33 | suite.Equal("CONNID", evt.StreamID()) 34 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 35 | } 36 | 37 | func (suite *EventsTestSuite) TestEventTraffic() { 38 | evt := mtglib.NewEventTraffic("CONNID", 1000, true) 39 | 40 | suite.Equal("CONNID", evt.StreamID()) 41 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 42 | } 43 | 44 | func (suite *EventsTestSuite) TestEventDomainFronting() { 45 | evt := mtglib.NewEventDomainFronting("CONNID") 46 | 47 | suite.Equal("CONNID", evt.StreamID()) 48 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 49 | } 50 | 51 | func (suite *EventsTestSuite) TestEventConcurrencyLimited() { 52 | evt := mtglib.NewEventConcurrencyLimited() 53 | 54 | suite.Empty(evt.StreamID()) 55 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 56 | } 57 | 58 | func (suite *EventsTestSuite) TestEventIPBlocklisted() { 59 | evt := mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")) 60 | 61 | suite.Empty(evt.StreamID()) 62 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 63 | suite.True(evt.IsBlockList) 64 | } 65 | 66 | func (suite *EventsTestSuite) TestEventIPAllowlisted() { 67 | evt := mtglib.NewEventIPAllowlisted(net.ParseIP("10.0.0.10")) 68 | 69 | suite.Empty(evt.StreamID()) 70 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 71 | suite.False(evt.IsBlockList) 72 | } 73 | 74 | func (suite *EventsTestSuite) TestEventReplayAttack() { 75 | evt := mtglib.NewEventReplayAttack("CONNID") 76 | 77 | suite.Equal("CONNID", evt.StreamID()) 78 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 79 | } 80 | 81 | func (suite *EventsTestSuite) TestEventIPListSize() { 82 | evt := mtglib.NewEventIPListSize(10, false) 83 | 84 | suite.Empty(evt.StreamID()) 85 | suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond) 86 | suite.Equal(10, evt.Size) 87 | suite.False(evt.IsBlockList) 88 | } 89 | 90 | func TestEvents(t *testing.T) { 91 | t.Parallel() 92 | suite.Run(t, &EventsTestSuite{}) 93 | } 94 | -------------------------------------------------------------------------------- /network/proxy_dialer_internal_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type ProxyDialerTestSuite struct { 13 | suite.Suite 14 | 15 | u *url.URL 16 | } 17 | 18 | func (suite *ProxyDialerTestSuite) SetupSuite() { 19 | u, _ := url.Parse("socks5://hello:world@10.0.0.10:3128") 20 | suite.u = u 21 | } 22 | 23 | func (suite *ProxyDialerTestSuite) TestSetupDefaults() { 24 | d := newProxyDialer(&DialerMock{}, suite.u).(*circuitBreakerDialer) //nolint: forcetypeassert 25 | suite.EqualValues(ProxyDialerOpenThreshold, d.openThreshold) 26 | suite.EqualValues(ProxyDialerHalfOpenTimeout, d.halfOpenTimeout) 27 | suite.EqualValues(ProxyDialerResetFailuresTimeout, d.resetFailuresTimeout) 28 | } 29 | 30 | func (suite *ProxyDialerTestSuite) TestSetupValuesAllOk() { 31 | query := url.Values{} 32 | query.Set("open_threshold", "30") 33 | query.Set("reset_failures_timeout", "1s") 34 | query.Set("half_open_timeout", "2s") 35 | suite.u.RawQuery = query.Encode() 36 | 37 | d := newProxyDialer(&DialerMock{}, suite.u).(*circuitBreakerDialer) //nolint: forcetypeassert 38 | suite.EqualValues(30, d.openThreshold) 39 | suite.EqualValues(2*time.Second, d.halfOpenTimeout) 40 | suite.EqualValues(time.Second, d.resetFailuresTimeout) 41 | } 42 | 43 | func (suite *ProxyDialerTestSuite) TestOpenThreshold() { 44 | query := url.Values{} 45 | params := []string{"-30", "aaa", "1.0", "-1.0"} 46 | 47 | for _, v := range params { 48 | param := v 49 | suite.T().Run(v, func(t *testing.T) { 50 | query.Set("open_threshold", param) 51 | suite.u.RawQuery = query.Encode() 52 | 53 | d := newProxyDialer(&DialerMock{}, suite.u).(*circuitBreakerDialer) //nolint: forcetypeassert 54 | assert.EqualValues(t, ProxyDialerOpenThreshold, d.openThreshold) 55 | }) 56 | } 57 | } 58 | 59 | func (suite *ProxyDialerTestSuite) TestHalfOpenTimeout() { 60 | query := url.Values{} 61 | params := []string{"-30", "30", "aaa", "-3.0", "3.0"} 62 | 63 | for _, v := range params { 64 | param := v 65 | suite.T().Run(v, func(t *testing.T) { 66 | query.Set("half_open_timeout", param) 67 | suite.u.RawQuery = query.Encode() 68 | 69 | d := newProxyDialer(&DialerMock{}, suite.u).(*circuitBreakerDialer) //nolint: forcetypeassert 70 | assert.EqualValues(t, ProxyDialerHalfOpenTimeout, d.halfOpenTimeout) 71 | }) 72 | } 73 | } 74 | 75 | func (suite *ProxyDialerTestSuite) TestResetFailuresTimeout() { 76 | query := url.Values{} 77 | params := []string{"-30", "30", "aaa", "-3.0", "3.0"} 78 | 79 | for _, v := range params { 80 | param := v 81 | suite.T().Run(v, func(t *testing.T) { 82 | query.Set("reset_failures_timeout", param) 83 | suite.u.RawQuery = query.Encode() 84 | 85 | d := newProxyDialer(&DialerMock{}, suite.u).(*circuitBreakerDialer) //nolint: forcetypeassert 86 | assert.EqualValues(t, ProxyDialerHalfOpenTimeout, d.halfOpenTimeout) 87 | }) 88 | } 89 | } 90 | 91 | func TestProxyDialer(t *testing.T) { 92 | t.Parallel() 93 | suite.Run(t, &ProxyDialerTestSuite{}) 94 | } 95 | -------------------------------------------------------------------------------- /mtglib/internal/faketls/record/record_test.go: -------------------------------------------------------------------------------- 1 | package record_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | type RecordTestSnapshot struct { 17 | Type int `json:"type"` 18 | Version int `json:"version"` 19 | Payload string `json:"payload"` 20 | Record string `json:"record"` 21 | } 22 | 23 | func (r RecordTestSnapshot) RecordBytes() []byte { 24 | data, _ := base64.StdEncoding.DecodeString(r.Record) 25 | 26 | return data 27 | } 28 | 29 | func (r RecordTestSnapshot) PayloadBytes() []byte { 30 | data, _ := base64.StdEncoding.DecodeString(r.Payload) 31 | 32 | return data 33 | } 34 | 35 | type RecordTestSuite struct { 36 | suite.Suite 37 | 38 | r *record.Record 39 | buf *bytes.Buffer 40 | } 41 | 42 | func (suite *RecordTestSuite) SetupTest() { 43 | suite.r = record.AcquireRecord() 44 | suite.buf = &bytes.Buffer{} 45 | } 46 | 47 | func (suite *RecordTestSuite) TearDownTest() { 48 | record.ReleaseRecord(suite.r) 49 | suite.buf.Reset() 50 | } 51 | 52 | func (suite *RecordTestSuite) TestIdempotent() { 53 | suite.r.Type = record.TypeApplicationData 54 | suite.r.Version = record.Version13 55 | 56 | suite.r.Payload.Write([]byte{1, 2, 3}) 57 | suite.NoError(suite.r.Dump(suite.buf)) 58 | 59 | suite.r.Reset() 60 | suite.NoError(suite.r.Read(suite.buf)) 61 | 62 | suite.Equal(0, suite.buf.Len()) 63 | suite.Equal(record.TypeApplicationData, suite.r.Type) 64 | suite.Equal(record.Version13, suite.r.Version) 65 | suite.Equal([]byte{1, 2, 3}, suite.r.Payload.Bytes()) 66 | } 67 | 68 | func (suite *RecordTestSuite) TestString() { 69 | _ = suite.r.String() 70 | } 71 | 72 | func (suite *RecordTestSuite) TestSnapshot() { 73 | files, err := os.ReadDir("testdata") 74 | suite.NoError(err) 75 | 76 | testData := map[string]string{} 77 | 78 | for _, f := range files { 79 | testData[f.Name()] = filepath.Join("testdata", f.Name()) 80 | } 81 | 82 | for name, pathV := range testData { 83 | path := pathV 84 | 85 | suite.T().Run(name, func(t *testing.T) { 86 | data, err := os.ReadFile(path) 87 | assert.NoError(t, err) 88 | 89 | snapshot := &RecordTestSnapshot{} 90 | assert.NoError(t, json.Unmarshal(data, snapshot)) 91 | 92 | rec := record.AcquireRecord() 93 | defer record.ReleaseRecord(rec) 94 | 95 | assert.NoError(t, rec.Read(bytes.NewReader(snapshot.RecordBytes()))) 96 | assert.Equal(t, snapshot.Type, int(rec.Type)) 97 | assert.Equal(t, snapshot.Version, int(rec.Version)) 98 | assert.Equal(t, snapshot.PayloadBytes(), rec.Payload.Bytes()) 99 | 100 | buf := &bytes.Buffer{} 101 | assert.NoError(t, rec.Dump(buf)) 102 | assert.Equal(t, snapshot.RecordBytes(), buf.Bytes()) 103 | }) 104 | } 105 | } 106 | 107 | func TestRecord(t *testing.T) { 108 | t.Parallel() 109 | suite.Run(t, &RecordTestSuite{}) 110 | } 111 | -------------------------------------------------------------------------------- /internal/config/type_statsd_tag_format_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/config" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type typeStatsdTagFormatTestStruct struct { 14 | Value config.TypeStatsdTagFormat `json:"value"` 15 | } 16 | 17 | type StatsdTagFormatTestSuite struct { 18 | suite.Suite 19 | } 20 | 21 | func (suite *StatsdTagFormatTestSuite) TestUnmarshalFail() { 22 | testData := []string{ 23 | "", 24 | "dogdog", 25 | } 26 | 27 | for _, v := range testData { 28 | data, err := json.Marshal(map[string]string{ 29 | "value": v, 30 | }) 31 | suite.NoError(err) 32 | 33 | suite.T().Run(v, func(t *testing.T) { 34 | assert.Error(t, json.Unmarshal(data, &typeStatsdTagFormatTestStruct{})) 35 | }) 36 | } 37 | } 38 | 39 | func (suite *StatsdTagFormatTestSuite) TestUnmarshalOk() { 40 | testData := []string{ 41 | config.TypeStatsdTagFormatInfluxdb, 42 | config.TypeStatsdTagFormatGraphite, 43 | config.TypeStatsdTagFormatDatadog, 44 | strings.ToUpper(config.TypeStatsdTagFormatInfluxdb), 45 | strings.ToUpper(config.TypeStatsdTagFormatGraphite), 46 | strings.ToUpper(config.TypeStatsdTagFormatDatadog), 47 | } 48 | 49 | for _, v := range testData { 50 | value := v 51 | 52 | data, err := json.Marshal(map[string]string{ 53 | "value": v, 54 | }) 55 | suite.NoError(err) 56 | 57 | suite.T().Run(v, func(t *testing.T) { 58 | testStruct := &typeStatsdTagFormatTestStruct{} 59 | assert.NoError(t, json.Unmarshal(data, testStruct)) 60 | assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) 61 | }) 62 | } 63 | } 64 | 65 | func (suite *StatsdTagFormatTestSuite) TestMarshalOk() { 66 | testData := []string{ 67 | config.TypeStatsdTagFormatInfluxdb, 68 | config.TypeStatsdTagFormatGraphite, 69 | config.TypeStatsdTagFormatDatadog, 70 | } 71 | 72 | for _, v := range testData { 73 | value := v 74 | 75 | suite.T().Run(v, func(t *testing.T) { 76 | testStruct := &typeStatsdTagFormatTestStruct{ 77 | Value: config.TypeStatsdTagFormat{ 78 | Value: value, 79 | }, 80 | } 81 | 82 | encodedJSON, err := json.Marshal(testStruct) 83 | assert.NoError(t, err) 84 | 85 | expectedJSON, err := json.Marshal(map[string]string{ 86 | "value": value, 87 | }) 88 | assert.NoError(t, err) 89 | 90 | assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) 91 | }) 92 | } 93 | } 94 | 95 | func (suite *StatsdTagFormatTestSuite) TestGet() { 96 | value := config.TypeStatsdTagFormat{} 97 | suite.Equal(config.TypeStatsdTagFormatDatadog, 98 | value.Get(config.TypeStatsdTagFormatDatadog)) 99 | 100 | suite.NoError(value.Set(config.TypeStatsdTagFormatInfluxdb)) 101 | suite.Equal(config.TypeStatsdTagFormatInfluxdb, 102 | value.Get(config.TypeStatsdTagFormatDatadog)) 103 | } 104 | 105 | func TestTypeStatsdTagFormat(t *testing.T) { 106 | t.Parallel() 107 | suite.Run(t, &StatsdTagFormatTestSuite{}) 108 | } 109 | -------------------------------------------------------------------------------- /internal/config/type_blocklist_uri_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/9seconds/mtg/v2/internal/config" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/suite" 13 | ) 14 | 15 | type typeBlocklistURITestStruct struct { 16 | Value config.TypeBlocklistURI `json:"value"` 17 | } 18 | 19 | type TypeBlocklistURITestSuite struct { 20 | suite.Suite 21 | 22 | directory string 23 | absDirectory string 24 | } 25 | 26 | func (suite *TypeBlocklistURITestSuite) SetupSuite() { 27 | dir, _ := os.Getwd() 28 | absDir, _ := filepath.Abs(dir) 29 | 30 | suite.directory = dir 31 | suite.absDirectory = absDir 32 | } 33 | 34 | func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() { 35 | testData := []string{ 36 | "gopher://lalala", 37 | "https:///paths", 38 | "h:/=", 39 | filepath.Join(suite.directory, "___"), 40 | filepath.Join(suite.absDirectory, "___"), 41 | suite.directory, 42 | suite.absDirectory, 43 | } 44 | 45 | for _, v := range testData { 46 | data, err := json.Marshal(map[string]string{ 47 | "value": v, 48 | }) 49 | suite.NoError(err) 50 | 51 | suite.T().Run(v, func(t *testing.T) { 52 | assert.Error(t, json.Unmarshal(data, &typeBlocklistURITestStruct{})) 53 | }) 54 | } 55 | } 56 | 57 | func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() { 58 | testData := []string{ 59 | "http://lalala", 60 | "https://lalala", 61 | "https://lalala/path", 62 | filepath.Join(suite.directory, "config.go"), 63 | filepath.Join(suite.absDirectory, "config.go"), 64 | } 65 | 66 | for _, v := range testData { 67 | value := v 68 | 69 | data, err := json.Marshal(map[string]string{ 70 | "value": v, 71 | }) 72 | suite.NoError(err) 73 | 74 | suite.T().Run(v, func(t *testing.T) { 75 | testStruct := &typeBlocklistURITestStruct{} 76 | 77 | assert.NoError(t, json.Unmarshal(data, testStruct)) 78 | assert.EqualValues(t, value, testStruct.Value.Get("")) 79 | 80 | if strings.HasPrefix(value, "http") { 81 | assert.True(t, testStruct.Value.IsRemote()) 82 | } else { 83 | assert.False(t, testStruct.Value.IsRemote()) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func (suite *TypeBlocklistURITestSuite) TestMarshalOk() { 90 | testStruct := &typeBlocklistURITestStruct{ 91 | Value: config.TypeBlocklistURI{ 92 | Value: "http://some.url/with/path", 93 | }, 94 | } 95 | 96 | data, err := json.Marshal(testStruct) 97 | suite.NoError(err) 98 | suite.JSONEq(`{"value": "http://some.url/with/path"}`, string(data)) 99 | } 100 | 101 | func (suite *TypeBlocklistURITestSuite) TestGet() { 102 | value := config.TypeBlocklistURI{} 103 | suite.Equal("/path", value.Get("/path")) 104 | 105 | suite.NoError(value.Set("http://lalala.ru")) 106 | suite.Equal("http://lalala.ru", value.Get("/path")) 107 | suite.Equal("http://lalala.ru", value.Get("")) 108 | } 109 | 110 | func TestTypeBlocklistURI(t *testing.T) { 111 | t.Parallel() 112 | suite.Run(t, &TypeBlocklistURITestSuite{}) 113 | } 114 | -------------------------------------------------------------------------------- /internal/config/type_prefer_ip_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/internal/config" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type typePreferIPTestStruct struct { 14 | Value config.TypePreferIP `json:"value"` 15 | } 16 | 17 | type TypePreferIPTestSuite struct { 18 | suite.Suite 19 | } 20 | 21 | func (suite *TypePreferIPTestSuite) TestUnmarshalFail() { 22 | testData := []string{ 23 | "", 24 | "prefer", 25 | "preferipv4", 26 | config.TypePreferIPPreferIPv4 + "_", 27 | } 28 | 29 | for _, v := range testData { 30 | data, err := json.Marshal(map[string]string{ 31 | "value": v, 32 | }) 33 | suite.NoError(err) 34 | 35 | suite.T().Run(v, func(t *testing.T) { 36 | assert.Error(t, json.Unmarshal(data, &typePreferIPTestStruct{})) 37 | }) 38 | } 39 | } 40 | 41 | func (suite *TypePreferIPTestSuite) TestUnmarshalOk() { 42 | testData := []string{ 43 | config.TypePreferIPPreferIPv4, 44 | config.TypePreferIPPreferIPv6, 45 | config.TypePreferOnlyIPv4, 46 | config.TypePreferOnlyIPv6, 47 | strings.ToTitle(config.TypePreferOnlyIPv4), 48 | strings.ToTitle(config.TypePreferOnlyIPv6), 49 | strings.ToTitle(config.TypePreferIPPreferIPv4), 50 | strings.ToTitle(config.TypePreferIPPreferIPv6), 51 | } 52 | 53 | for _, v := range testData { 54 | value := v 55 | 56 | data, err := json.Marshal(map[string]string{ 57 | "value": v, 58 | }) 59 | suite.NoError(err) 60 | 61 | suite.T().Run(v, func(t *testing.T) { 62 | testStruct := &typePreferIPTestStruct{} 63 | assert.NoError(t, json.Unmarshal(data, testStruct)) 64 | assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) 65 | }) 66 | } 67 | } 68 | 69 | func (suite *TypePreferIPTestSuite) TestMarshalOk() { 70 | testData := []string{ 71 | config.TypePreferIPPreferIPv4, 72 | config.TypePreferIPPreferIPv6, 73 | config.TypePreferOnlyIPv4, 74 | config.TypePreferOnlyIPv6, 75 | } 76 | 77 | for _, v := range testData { 78 | value := v 79 | 80 | suite.T().Run(v, func(t *testing.T) { 81 | testStruct := &typePreferIPTestStruct{ 82 | Value: config.TypePreferIP{ 83 | Value: value, 84 | }, 85 | } 86 | 87 | encodedJSON, err := json.Marshal(testStruct) 88 | assert.NoError(t, err) 89 | 90 | expectedJSON, err := json.Marshal(map[string]string{ 91 | "value": value, 92 | }) 93 | assert.NoError(t, err) 94 | 95 | assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) 96 | }) 97 | } 98 | } 99 | 100 | func (suite *TypePreferIPTestSuite) TestGet() { 101 | value := config.TypePreferIP{} 102 | suite.Equal(config.TypePreferIPPreferIPv4, 103 | value.Get(config.TypePreferIPPreferIPv4)) 104 | 105 | suite.NoError(value.Set(config.TypePreferIPPreferIPv6)) 106 | suite.Equal(config.TypePreferIPPreferIPv6, 107 | value.Get(config.TypePreferIPPreferIPv4)) 108 | } 109 | 110 | func TestTypePreferIP(t *testing.T) { 111 | t.Parallel() 112 | suite.Run(t, &TypePreferIPTestSuite{}) 113 | } 114 | -------------------------------------------------------------------------------- /events/init.go: -------------------------------------------------------------------------------- 1 | // Events has a default implementations of EventStream for mtglib. 2 | // 3 | // Please see documentation for [mtglib.EventStream] interface to get an idea 4 | // of such an abstraction. This package has implementations for the default 5 | // event stream. 6 | // 7 | // Default event stream has a list of its own concepts. First, all it does is a 8 | // routing of messages to known observers. It takes an event, defines its type 9 | // and pass this message to a method of the observer. 10 | // 11 | // There might be many observers, but default event stream has a guarantee 12 | // though. It uses StreamID as a sharding key and guarantees that a message 13 | // with the same StreamID will be devlivered to the same observer instance. So, 14 | // each producer is guarateed to get all relevant messages related to the same 15 | // session. It is not possible that it will get EventFinish if it has not seen 16 | // EventStart for that session yet. 17 | package events 18 | 19 | import "github.com/9seconds/mtg/v2/mtglib" 20 | 21 | // Observer is an instance that listens for the incoming events. 22 | // 23 | // As it is said in the package description, the default event stream 24 | // guarantees that all events with the same StreamID are going to be routed to 25 | // the same instance of the observer. So, there is no need to synchronize 26 | // information about streams between many observers instances, they can have 27 | // their local storage. 28 | type Observer interface { 29 | // EventStart reacts on incoming mtglib.EventStart event. 30 | EventStart(mtglib.EventStart) 31 | 32 | // EventFinish reacts on incoming mtglib.EventFinish event. 33 | EventFinish(mtglib.EventFinish) 34 | 35 | // EventConnectedToDC reacts on incoming mtglib.EventConnectedToDC 36 | // event. 37 | EventConnectedToDC(mtglib.EventConnectedToDC) 38 | 39 | // EventDomainFronting reacts on incoming mtglib.EventDomainFronting 40 | // event. 41 | EventDomainFronting(mtglib.EventDomainFronting) 42 | 43 | // EventTraffic reacts on incoming mtglib.EventTraffic event. 44 | EventTraffic(mtglib.EventTraffic) 45 | 46 | // EventConcurrencyLimited reacts on incoming 47 | // mtglib.EventConcurrencyLimited event. 48 | EventConcurrencyLimited(mtglib.EventConcurrencyLimited) 49 | 50 | // EventIPBlocklisted reacts on incoming mtglib.EventIPBlocklisted event. 51 | EventIPBlocklisted(mtglib.EventIPBlocklisted) 52 | 53 | // EventReplayAttack reacts on incoming mtglib.EventReplayAttack event. 54 | EventReplayAttack(mtglib.EventReplayAttack) 55 | 56 | // EventIPListSize reacts on incoming mtglib.EventIPListSize 57 | EventIPListSize(mtglib.EventIPListSize) 58 | 59 | // Shutdown stop observer. Default event stream guarantees: 60 | // 1. If shutdown is executed, it is executed only once 61 | // 2. Observer won't receieve any new message after this 62 | // function call. 63 | Shutdown() 64 | } 65 | 66 | // ObserverFactory creates a new instance of the observer. 67 | // 68 | // Default event stream creates a small set of goroutines to manage incoming 69 | // messages. Each message is routed to an appropriate observer based on a 70 | // sharding key, stream id. So, it is possible that an instance of mtg will 71 | // have many observer instances, not a single one. 72 | type ObserverFactory func() Observer 73 | -------------------------------------------------------------------------------- /mtglib/secret_test.go: -------------------------------------------------------------------------------- 1 | package mtglib_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/9seconds/mtg/v2/mtglib" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type SecretTestSuite struct { 14 | suite.Suite 15 | } 16 | 17 | func (suite *SecretTestSuite) TestParseSecret() { 18 | secretData, _ := hex.DecodeString("d11c6cbbd9efe7fed5bc0db220b09665") 19 | s := mtglib.Secret{ 20 | Host: "google.com", 21 | } 22 | 23 | copy(s.Key[:], secretData) 24 | 25 | testData := map[string]string{ 26 | "hex": "eed11c6cbbd9efe7fed5bc0db220b09665676f6f676c652e636f6d", 27 | "base64": "7tEcbLvZ7-f-1bwNsiCwlmVnb29nbGUuY29t", 28 | } 29 | 30 | for name, value := range testData { 31 | param := value 32 | 33 | suite.T().Run(name, func(t *testing.T) { 34 | parsed, err := mtglib.ParseSecret(param) 35 | assert.NoError(t, err) 36 | assert.Equal(t, s.Key, parsed.Key) 37 | assert.Equal(t, s.Host, parsed.Host) 38 | 39 | newSecret := mtglib.Secret{} 40 | assert.NoError(t, newSecret.UnmarshalText([]byte(param))) 41 | assert.Equal(t, s.Key, newSecret.Key) 42 | assert.Equal(t, s.Host, newSecret.Host) 43 | }) 44 | } 45 | } 46 | 47 | func (suite *SecretTestSuite) TestSerialize() { 48 | s := mtglib.Secret{} 49 | 50 | data, err := s.MarshalText() 51 | suite.NoError(err) 52 | suite.Empty(data) 53 | 54 | secretData, _ := hex.DecodeString("d11c6cbbd9efe7fed5bc0db220b09665") 55 | s = mtglib.Secret{ 56 | Host: "google.com", 57 | } 58 | 59 | copy(s.Key[:], secretData) 60 | 61 | suite.Equal("eed11c6cbbd9efe7fed5bc0db220b09665676f6f676c652e636f6d", s.Hex()) 62 | suite.Equal("7tEcbLvZ7-f-1bwNsiCwlmVnb29nbGUuY29t", s.Base64()) 63 | } 64 | 65 | func (suite *SecretTestSuite) TestMarshalData() { 66 | secretData, _ := hex.DecodeString("d11c6cbbd9efe7fed5bc0db220b09665") 67 | s := mtglib.Secret{ 68 | Host: "google.com", 69 | } 70 | 71 | copy(s.Key[:], secretData) 72 | 73 | data, err := json.Marshal(&s) 74 | suite.NoError(err) 75 | suite.Equal(string(data), `"7tEcbLvZ7-f-1bwNsiCwlmVnb29nbGUuY29t"`) 76 | } 77 | 78 | func (suite *SecretTestSuite) TestIncorrectSecret() { 79 | testData := []string{ 80 | "aaa", 81 | "d11c6cbbd9efe7fed5bc0db220b09665", 82 | "ddd11c6cbbd9efe7fed5bc0db220b09665", 83 | "+ueJ0q91t5XOnFYP8Xac3A", 84 | "eed11c6cbbd9efe7fed5bc0db220b09665", 85 | "eed11c6cbbd9efe7fed5bc0db220b096", 86 | "ed11c6cbbd9efe7fed5bc0db220b09665", 87 | "", 88 | "+**", 89 | "ee", 90 | "efd11c6cbbd9efe7fed5bc0db220b09665", 91 | } 92 | 93 | for _, v := range testData { 94 | param := v 95 | 96 | suite.T().Run(param, func(t *testing.T) { 97 | _, err := mtglib.ParseSecret(param) 98 | assert.Error(t, err) 99 | }) 100 | } 101 | } 102 | 103 | func (suite *SecretTestSuite) TestInvariant() { 104 | generated := mtglib.GenerateSecret("google.com") 105 | 106 | parsed, err := mtglib.ParseSecret(generated.Hex()) 107 | suite.NoError(err) 108 | suite.Equal(generated.Key, parsed.Key) 109 | suite.Equal(generated.Host, parsed.Host) 110 | suite.Equal("google.com", parsed.Host) 111 | } 112 | 113 | func (suite *SecretTestSuite) TestValid() { 114 | s := mtglib.Secret{} 115 | suite.False(s.Valid()) 116 | 117 | s.Key[0] = 1 118 | suite.False(s.Valid()) 119 | 120 | s.Host = "11" 121 | suite.True(s.Valid()) 122 | } 123 | 124 | func TestSecret(t *testing.T) { 125 | t.Parallel() 126 | suite.Run(t, &SecretTestSuite{}) 127 | } 128 | -------------------------------------------------------------------------------- /events/event_stream.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "runtime" 7 | 8 | "github.com/9seconds/mtg/v2/mtglib" 9 | "github.com/OneOfOne/xxhash" 10 | ) 11 | 12 | // EventStream is a default implementation of the [mtglib.EventStream] 13 | // interface. 14 | // 15 | // EventStream manages a set of goroutines, observers. Main 16 | // responsibility of the event stream is to route an event to relevant 17 | // observer based on some hash so each observer will have all events 18 | // which belong to some stream id. 19 | // 20 | // Thus, EventStream can spawn many observers. 21 | type EventStream struct { 22 | ctx context.Context 23 | ctxCancel context.CancelFunc 24 | chans []chan mtglib.Event 25 | } 26 | 27 | // Send starts delivering of the message to observer with respect to a 28 | // given context If context is closed, message could be not delivered. 29 | func (e EventStream) Send(ctx context.Context, evt mtglib.Event) { 30 | var chanNo uint32 31 | 32 | if streamID := evt.StreamID(); streamID != "" { 33 | chanNo = xxhash.ChecksumString32(streamID) 34 | } else { 35 | chanNo = rand.Uint32() 36 | } 37 | 38 | select { 39 | case <-ctx.Done(): 40 | case <-e.ctx.Done(): 41 | case e.chans[int(chanNo)%len(e.chans)] <- evt: 42 | } 43 | } 44 | 45 | // Shutdown stops an event stream pipeline. 46 | func (e EventStream) Shutdown() { 47 | e.ctxCancel() 48 | } 49 | 50 | // NewEventStream builds a new default event stream. 51 | // 52 | // If you give an empty array of observers, then NoopObserver is going 53 | // to be used. If you give many observers, then they will process a 54 | // message concurrently. 55 | func NewEventStream(observerFactories []ObserverFactory) EventStream { 56 | if len(observerFactories) == 0 { 57 | observerFactories = append(observerFactories, NewNoopObserver) 58 | } 59 | 60 | ctx, cancel := context.WithCancel(context.Background()) 61 | rv := EventStream{ 62 | ctx: ctx, 63 | ctxCancel: cancel, 64 | chans: make([]chan mtglib.Event, runtime.NumCPU()), 65 | } 66 | 67 | for i := 0; i < runtime.NumCPU(); i++ { 68 | rv.chans[i] = make(chan mtglib.Event, 1) 69 | 70 | if len(observerFactories) == 1 { 71 | go eventStreamProcessor(ctx, rv.chans[i], observerFactories[0]()) 72 | } else { 73 | go eventStreamProcessor(ctx, rv.chans[i], newMultiObserver(observerFactories)) 74 | } 75 | } 76 | 77 | return rv 78 | } 79 | 80 | func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { //nolint: cyclop 81 | defer observer.Shutdown() 82 | 83 | for { 84 | select { 85 | case <-ctx.Done(): 86 | return 87 | case evt := <-eventChan: 88 | switch typedEvt := evt.(type) { 89 | case mtglib.EventTraffic: 90 | observer.EventTraffic(typedEvt) 91 | case mtglib.EventStart: 92 | observer.EventStart(typedEvt) 93 | case mtglib.EventFinish: 94 | observer.EventFinish(typedEvt) 95 | case mtglib.EventConnectedToDC: 96 | observer.EventConnectedToDC(typedEvt) 97 | case mtglib.EventDomainFronting: 98 | observer.EventDomainFronting(typedEvt) 99 | case mtglib.EventIPBlocklisted: 100 | observer.EventIPBlocklisted(typedEvt) 101 | case mtglib.EventConcurrencyLimited: 102 | observer.EventConcurrencyLimited(typedEvt) 103 | case mtglib.EventReplayAttack: 104 | observer.EventReplayAttack(typedEvt) 105 | case mtglib.EventIPListSize: 106 | observer.EventIPListSize(typedEvt) 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /logger/zerolog.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/9seconds/mtg/v2/mtglib" 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | const loggerFieldName = "logger" 11 | 12 | type zeroLogContextVarType uint8 13 | 14 | const ( 15 | zeroLogContextVarTypeUnknown zeroLogContextVarType = iota 16 | zeroLogContextVarTypeStr 17 | zeroLogContextVarTypeInt 18 | zeroLogContextVarTypeJSON 19 | ) 20 | 21 | type zeroLogContext struct { 22 | name string 23 | log *zerolog.Logger 24 | 25 | ctxVarType zeroLogContextVarType 26 | ctxVarName string 27 | ctxVarStr string 28 | ctxVarInt int 29 | 30 | parent *zeroLogContext 31 | } 32 | 33 | func (z *zeroLogContext) Named(name string) mtglib.Logger { 34 | loggerName := z.name 35 | if loggerName == "" { 36 | loggerName = name 37 | } else { 38 | loggerName += "." + name 39 | } 40 | 41 | return &zeroLogContext{ 42 | name: loggerName, 43 | log: z.log, 44 | parent: z, 45 | } 46 | } 47 | 48 | func (z *zeroLogContext) BindInt(name string, value int) mtglib.Logger { 49 | return &zeroLogContext{ 50 | name: z.name, 51 | log: z.log, 52 | ctxVarType: zeroLogContextVarTypeInt, 53 | ctxVarInt: value, 54 | ctxVarName: name, 55 | parent: z, 56 | } 57 | } 58 | 59 | func (z *zeroLogContext) BindStr(name, value string) mtglib.Logger { 60 | return &zeroLogContext{ 61 | name: z.name, 62 | log: z.log, 63 | ctxVarType: zeroLogContextVarTypeStr, 64 | ctxVarStr: value, 65 | ctxVarName: name, 66 | parent: z, 67 | } 68 | } 69 | 70 | func (z *zeroLogContext) BindJSON(name, value string) mtglib.Logger { 71 | return &zeroLogContext{ 72 | name: z.name, 73 | log: z.log, 74 | ctxVarType: zeroLogContextVarTypeJSON, 75 | ctxVarName: name, 76 | ctxVarStr: value, 77 | parent: z, 78 | } 79 | } 80 | 81 | func (z *zeroLogContext) Printf(format string, args ...interface{}) { 82 | z.Debug(fmt.Sprintf(format, args...)) 83 | } 84 | 85 | func (z *zeroLogContext) Info(msg string) { 86 | z.InfoError(msg, nil) 87 | } 88 | 89 | func (z *zeroLogContext) Warning(msg string) { 90 | z.WarningError(msg, nil) 91 | } 92 | 93 | func (z *zeroLogContext) Debug(msg string) { 94 | z.DebugError(msg, nil) 95 | } 96 | 97 | func (z *zeroLogContext) InfoError(msg string, err error) { 98 | z.emitLog(z.log.Info(), msg, err) 99 | } 100 | 101 | func (z *zeroLogContext) WarningError(msg string, err error) { 102 | z.emitLog(z.log.Warn(), msg, err) 103 | } 104 | 105 | func (z *zeroLogContext) DebugError(msg string, err error) { 106 | z.emitLog(z.log.Debug(), msg, err) 107 | } 108 | 109 | func (z *zeroLogContext) emitLog(evt *zerolog.Event, msg string, err error) { 110 | z.attachCtx(evt) 111 | 112 | for current := z.parent; current != nil; current = current.parent { 113 | current.attachCtx(evt) 114 | } 115 | 116 | evt.Str(loggerFieldName, z.name).Err(err).Msg(msg) 117 | } 118 | 119 | func (z *zeroLogContext) attachCtx(evt *zerolog.Event) { 120 | switch z.ctxVarType { 121 | case zeroLogContextVarTypeStr: 122 | evt.Str(z.ctxVarName, z.ctxVarStr) 123 | case zeroLogContextVarTypeInt: 124 | evt.Int(z.ctxVarName, z.ctxVarInt) 125 | case zeroLogContextVarTypeJSON: 126 | evt.RawJSON(z.ctxVarName, []byte(z.ctxVarStr)) 127 | case zeroLogContextVarTypeUnknown: 128 | } 129 | } 130 | 131 | // NewZeroLogger returns a logger which is using rs/zerolog library. 132 | func NewZeroLogger(log zerolog.Logger) mtglib.Logger { 133 | return &zeroLogContext{ 134 | log: &log, 135 | } 136 | } 137 | --------------------------------------------------------------------------------