Add fetching of addresses from proxyGetConfig endpoint

This commit is contained in:
9seconds
2026-02-24 12:55:16 +01:00
parent 908842063a
commit 94d46d2c65
11 changed files with 419 additions and 80 deletions
-16
View File
@@ -1,16 +0,0 @@
package dc_test
import (
"testing"
"github.com/9seconds/mtg/v2/mtglib/internal/dc"
"github.com/stretchr/testify/assert"
)
func TestAddr(t *testing.T) {
t.Parallel()
addr := dc.Addr{Network: "tcp4", Address: "127.0.0.1:443"}
assert.Equal(t, "127.0.0.1:443", addr.String())
}
+20 -13
View File
@@ -1,5 +1,10 @@
package dc package dc
import (
"context"
"time"
)
type preferIP uint8 type preferIP uint8
const ( const (
@@ -10,7 +15,18 @@ const (
) )
const ( const (
// Default DC to connect to if not sure.
DefaultDC = 2 DefaultDC = 2
// How often should we request updates from
// https://core.telegram.org/getProxyConfig
PublicConfigUpdateEach = time.Hour
PublicConfigUpdateURLv4 = "https://core.telegram.org/getProxyConfig"
PublicConfigUpdateURLv6 = "https://core.telegram.org/getProxyConfigV6"
// How often should we extract hosts from Telegram using help.getConfig
// method.
OwnConfigUpdateEach = time.Hour
) )
type Logger interface { type Logger interface {
@@ -18,6 +34,10 @@ type Logger interface {
WarningError(msg string, err error) WarningError(msg string, err error)
} }
type Updater interface {
Run(ctx context.Context)
}
var ( var (
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30
defaultDCAddrSet = dcAddrSet{ defaultDCAddrSet = dcAddrSet{
@@ -57,17 +77,4 @@ var (
}, },
}, },
} }
defaultDCOverridesAddrSet = dcAddrSet{
v4: map[int][]Addr{
203: {
{Network: "tcp4", Address: "91.105.192.100:443"},
},
},
v6: map[int][]Addr{
203: {
{Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"},
},
},
}
) )
+43
View File
@@ -0,0 +1,43 @@
package dc
import (
"context"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type LoggerMock struct {
mock.Mock
}
func (m *LoggerMock) Info(msg string) {
m.Called(msg)
}
func (m *LoggerMock) WarningError(msg string, err error) {
m.Called(msg, err)
}
type UpdaterTestSuiteBase struct {
suite.Suite
ctx context.Context
ctxCancel context.CancelFunc
loggerMock *LoggerMock
}
func (s *UpdaterTestSuiteBase) SetupTest() {
ctx, cancel := context.WithCancel(context.Background())
s.loggerMock = &LoggerMock{}
s.loggerMock.On("Info", mock.AnythingOfType("string"))
s.loggerMock.On("WarningError", mock.AnythingOfType("string"), mock.Anything)
s.ctx = ctx
s.ctxCancel = cancel
}
func (s *UpdaterTestSuiteBase) TearDownTest() {
s.ctxCancel()
}
@@ -0,0 +1,93 @@
package dc
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
)
var publicConfigRe = regexp.MustCompile(`^\s*proxy_for\s+(\d+)\s+(\S+?)?;\s*$`)
type PublicConfigUpdater struct {
updater
http *http.Client
tg *Telegram
}
func (p PublicConfigUpdater) Run(ctx context.Context, url, network string) {
p.run(ctx, func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
panic(err)
}
resp, err := p.http.Do(req)
if err != nil {
if resp != nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
return fmt.Errorf("cannot fetch url %s: %w", url, err)
}
if resp.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("unexpected status code from %s: %d", url, resp.StatusCode)
}
scanner := bufio.NewScanner(resp.Body)
addrs := map[int][]Addr{}
for scanner.Scan() {
matches := publicConfigRe.FindStringSubmatch(scanner.Text())
if len(matches) != 3 {
continue
}
dc, err := strconv.Atoi(matches[1])
if err != nil {
continue
}
switch dc {
// this is a list of DC we currently support. Other are ignored.
case 203: // CDN DC
p.logger.Info(fmt.Sprintf("found %s address for DC %d", matches[2], dc))
addrs[dc] = append(addrs[dc], Addr{
Network: network,
Address: matches[2],
})
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("cannot read response body from %s: %w", url, err)
}
p.tg.lock.Lock()
defer p.tg.lock.Unlock()
if network == "tcp4" {
p.tg.view.publicConfigs.v4 = addrs
} else {
p.tg.view.publicConfigs.v6 = addrs
}
return nil
})
}
func NewPublicConfigUpdater(tg *Telegram, logger Logger, client *http.Client) PublicConfigUpdater {
return PublicConfigUpdater{
updater: updater{
logger: logger,
period: PublicConfigUpdateEach,
},
http: client,
tg: tg,
}
}
@@ -0,0 +1,135 @@
package dc
import (
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type PublicConfigUpdaterTestSuite struct {
UpdaterTestSuiteBase
u PublicConfigUpdater
lock sync.Mutex
srv *httptest.Server
responseHandler func(w http.ResponseWriter)
}
func (s *PublicConfigUpdaterTestSuite) SetupSuite() {
s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.lock.Lock()
s.responseHandler(w)
s.lock.Unlock()
}))
}
func (s *PublicConfigUpdaterTestSuite) TearDownSuite() {
s.srv.Close()
}
func (s *PublicConfigUpdaterTestSuite) SetupTest() {
s.UpdaterTestSuiteBase.SetupTest()
tg, err := New("prefer-ipv4")
require.NoError(s.T(), err)
s.u = NewPublicConfigUpdater(tg, s.loggerMock, s.srv.Client())
}
func (s *PublicConfigUpdaterTestSuite) Test502StatusCode() {
done := false
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusBadGateway)
done = true
}
go s.u.Run(s.ctx, s.srv.URL, "tcp4")
s.Eventually(func() bool {
s.lock.Lock()
defer s.lock.Unlock()
return done
}, time.Second, 10*time.Millisecond)
s.Len(s.u.tg.view.publicConfigs.v4, 0)
}
func (s *PublicConfigUpdaterTestSuite) TestEmptyFile() {
done := false
s.responseHandler = func(w http.ResponseWriter) {
done = true
w.WriteHeader(http.StatusOK)
}
go s.u.Run(s.ctx, s.srv.URL, "tcp4")
s.Eventually(func() bool {
s.lock.Lock()
defer s.lock.Unlock()
return done
}, time.Second, 10*time.Millisecond)
s.Len(s.u.tg.view.publicConfigs.v4, 0)
}
func (s *PublicConfigUpdaterTestSuite) TestGarbage() {
result := `
proxy_for -1 -1;
proxy_for 100 100.10.0.0:3333;
lala 0 0
`
done := false
s.responseHandler = func(w http.ResponseWriter) {
done = true
w.WriteHeader(http.StatusOK)
w.Write([]byte(result))
}
go s.u.Run(s.ctx, s.srv.URL, "tcp4")
s.Eventually(func() bool {
s.lock.Lock()
defer s.lock.Unlock()
return done
}, time.Second, 10*time.Millisecond)
s.Len(s.u.tg.view.publicConfigs.v4, 0)
}
func (s *PublicConfigUpdaterTestSuite) TestOk() {
result := `
proxy_for 203 100.10.0.0:3333;
proxy_for -100 101.10.0.0:3333;
`
done := false
s.responseHandler = func(w http.ResponseWriter) {
done = true
w.WriteHeader(http.StatusOK)
w.Write([]byte(result))
}
go s.u.Run(s.ctx, s.srv.URL, "tcp4")
s.Eventually(func() bool {
s.lock.Lock()
defer s.lock.Unlock()
return done
}, time.Second, 10*time.Millisecond)
s.Len(s.u.tg.view.publicConfigs.v4, 1)
s.Len(s.u.tg.view.publicConfigs.v4[203], 1)
s.Equal("100.10.0.0:3333", s.u.tg.view.publicConfigs.v4[203][0].Address)
}
func TestPublicConfigUpdater(t *testing.T) {
suite.Run(t, &PublicConfigUpdaterTestSuite{})
}
+8 -35
View File
@@ -1,17 +1,23 @@
package dc package dc
import ( import (
"context"
"fmt" "fmt"
"net"
"strings" "strings"
"sync"
) )
type Telegram struct { type Telegram struct {
ctx context.Context
lock sync.RWMutex
view dcView view dcView
preferIP preferIP preferIP preferIP
} }
func (t *Telegram) GetAddresses(dc int) []Addr { func (t *Telegram) GetAddresses(dc int) []Addr {
t.lock.RLock()
defer t.lock.RUnlock()
switch t.preferIP { switch t.preferIP {
case preferIPOnlyIPv4: case preferIPOnlyIPv4:
return t.view.getV4(dc) return t.view.getV4(dc)
@@ -24,7 +30,7 @@ func (t *Telegram) GetAddresses(dc int) []Addr {
return append(t.view.getV6(dc), t.view.getV4(dc)...) return append(t.view.getV6(dc), t.view.getV4(dc)...)
} }
func New(ipPreference string, userOverrides map[int][]string) (*Telegram, error) { func New(ipPreference string) (*Telegram, error) {
var pref preferIP var pref preferIP
switch strings.ToLower(ipPreference) { switch strings.ToLower(ipPreference) {
@@ -40,40 +46,7 @@ func New(ipPreference string, userOverrides map[int][]string) (*Telegram, error)
return nil, fmt.Errorf("unknown ip preference %s", ipPreference) return nil, fmt.Errorf("unknown ip preference %s", ipPreference)
} }
overrides := dcAddrSet{
v4: map[int][]Addr{},
v6: map[int][]Addr{},
}
for dc, addrs := range userOverrides {
for _, addr := range addrs {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("incorrect host %s: %w", addr, err)
}
parsed := net.ParseIP(host)
if parsed == nil {
return nil, fmt.Errorf("incorrect host %s", addr)
}
if parsed.To4() != nil {
overrides.v4[dc] = append(overrides.v4[dc], Addr{
Network: "tcp4",
Address: addr,
})
} else {
overrides.v6[dc] = append(overrides.v6[dc], Addr{
Network: "tcp6",
Address: addr,
})
}
}
}
return &Telegram{ return &Telegram{
view: dcView{
overrides: overrides,
},
preferIP: pref, preferIP: pref,
}, nil }, nil
} }
+39
View File
@@ -0,0 +1,39 @@
package dc
import (
"context"
"time"
)
type updater struct {
logger Logger
period time.Duration
}
func (u updater) run(ctx context.Context, callback func() error) {
ticker := time.NewTicker(u.period)
defer func() {
ticker.Stop()
select {
case <-ticker.C:
default:
}
}()
for {
u.logger.Info("start update")
if err := callback(); err != nil {
u.logger.WarningError("cannot update: %w", err)
}
u.logger.Info("updated")
select {
case <-ctx.Done():
u.logger.Info("stop updating")
return
case <-ticker.C:
}
}
}
+55
View File
@@ -0,0 +1,55 @@
package dc
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type UpdaterTestSuite struct {
UpdaterTestSuiteBase
u updater
}
func (s *UpdaterTestSuite) SetupTest() {
s.UpdaterTestSuiteBase.SetupTest()
s.u = updater{
logger: s.loggerMock,
period: 100 * time.Millisecond,
}
}
func (s *UpdaterTestSuite) TestPeriodicUpdates() {
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
lock := &sync.Mutex{}
collected := []time.Time{}
go s.u.run(s.ctx, func() error {
select {
case <-s.ctx.Done():
case value := <-ticker.C:
lock.Lock()
collected = append(collected, value)
lock.Unlock()
}
return nil
})
s.Eventually(func() bool {
lock.Lock()
defer lock.Unlock()
return len(collected) == 3
}, time.Second, 10*time.Millisecond)
}
func TestUpdater(t *testing.T) {
t.Parallel()
suite.Run(t, &UpdaterTestSuite{})
}
+6 -5
View File
@@ -1,20 +1,21 @@
package dc package dc
type dcView struct { type dcView struct {
overrides dcAddrSet publicConfigs dcAddrSet
ownConfigs dcAddrSet
} }
func (d dcView) getV4(dc int) []Addr { func (d dcView) getV4(dc int) []Addr {
addrs := d.overrides.getV4(dc) addrs := d.publicConfigs.getV4(dc)
addrs = append(addrs, defaultDCOverridesAddrSet.getV4(dc)...) addrs = append(addrs, d.ownConfigs.getV4(dc)...)
addrs = append(addrs, defaultDCAddrSet.getV4(dc)...) addrs = append(addrs, defaultDCAddrSet.getV4(dc)...)
return addrs return addrs
} }
func (d dcView) getV6(dc int) []Addr { func (d dcView) getV6(dc int) []Addr {
addrs := d.overrides.getV6(dc) addrs := d.publicConfigs.getV6(dc)
addrs = append(addrs, defaultDCOverridesAddrSet.getV6(dc)...) addrs = append(addrs, d.ownConfigs.getV6(dc)...)
addrs = append(addrs, defaultDCAddrSet.getV6(dc)...) addrs = append(addrs, defaultDCAddrSet.getV6(dc)...)
return addrs return addrs
+7 -9
View File
@@ -16,7 +16,7 @@ type ViewTestSuite struct {
func (suite *ViewTestSuite) SetupSuite() { func (suite *ViewTestSuite) SetupSuite() {
suite.view = dcView{ suite.view = dcView{
overrides: dcAddrSet{ publicConfigs: dcAddrSet{
v4: map[int][]Addr{ v4: map[int][]Addr{
111: { 111: {
{Network: "tcp4", Address: "127.0.0.1:443"}, {Network: "tcp4", Address: "127.0.0.1:443"},
@@ -37,15 +37,14 @@ func (suite *ViewTestSuite) SetupSuite() {
func (suite *ViewTestSuite) TestGetV4() { func (suite *ViewTestSuite) TestGetV4() {
testData := map[int][]Addr{ testData := map[int][]Addr{
111: { 111: {
{"tcp4", "127.0.0.1:443"}, {Network: "tcp4", Address: "127.0.0.1:443"},
}, },
203: { 203: {
{"tcp4", "127.0.0.2:443"}, {Network: "tcp4", Address: "127.0.0.2:443"},
{"tcp4", "91.105.192.100:443"},
}, },
2: { 2: {
{"tcp4", "149.154.167.51:443"}, {Network: "tcp4", Address: "149.154.167.51:443"},
{"tcp4", "95.161.76.100:443"}, {Network: "tcp4", Address: "95.161.76.100:443"},
}, },
} }
@@ -60,11 +59,10 @@ func (suite *ViewTestSuite) TestGetV6() {
testData := map[int][]Addr{ testData := map[int][]Addr{
111: {}, 111: {},
203: { 203: {
{"tcp6", "xxx"}, {Network: "tcp6", Address: "xxx"},
{"tcp6", "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"},
}, },
1: { 1: {
{"tcp6", "[2001:b28:f23d:f001::a]:443"}, {Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"},
}, },
} }
+13 -2
View File
@@ -297,12 +297,15 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
return nil, fmt.Errorf("invalid settings: %w", err) return nil, fmt.Errorf("invalid settings: %w", err)
} }
tg, err := dc.New(opts.getPreferIP(), opts.DCOverrides) tg, err := dc.New(opts.getPreferIP())
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot build telegram dc fetcher: %w", err) return nil, fmt.Errorf("cannot build telegram dc fetcher: %w", err)
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
logger := opts.getLogger("proxy")
updatersLogger := logger.Named("telegram-updaters")
proxy := &Proxy{ proxy := &Proxy{
ctx: ctx, ctx: ctx,
ctxCancel: cancel, ctxCancel: cancel,
@@ -312,7 +315,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
blocklist: opts.IPBlocklist, blocklist: opts.IPBlocklist,
allowlist: opts.IPAllowlist, allowlist: opts.IPAllowlist,
eventStream: opts.EventStream, eventStream: opts.EventStream,
logger: opts.getLogger("proxy"), logger: logger,
domainFrontingPort: opts.getDomainFrontingPort(), domainFrontingPort: opts.getDomainFrontingPort(),
tolerateTimeSkewness: opts.getTolerateTimeSkewness(), tolerateTimeSkewness: opts.getTolerateTimeSkewness(),
allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
@@ -322,6 +325,14 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
}, },
} }
publicConfigUpdater := dc.NewPublicConfigUpdater(
tg,
updatersLogger.Named("public-config"),
opts.Network.MakeHTTPClient(nil),
)
go publicConfigUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4")
go publicConfigUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6")
pool, err := ants.NewPoolWithFunc(opts.getConcurrency(), pool, err := ants.NewPoolWithFunc(opts.getConcurrency(),
func(arg any) { func(arg any) {
proxy.ServeConn(arg.(essentials.Conn)) //nolint: forcetypeassert proxy.ServeConn(arg.(essentials.Conn)) //nolint: forcetypeassert