Skip to content

Commit b3f61f9

Browse files
authored
feat: add DNS tunnel support (#1507)
* feat: add DNS tunnel support * add dnstt config validation function * add nil dnstt config test, make dnstt config optional in template * recompile genconfig with recent changes
1 parent 7f7603d commit b3f61f9

File tree

10 files changed

+236
-46
lines changed

10 files changed

+236
-46
lines changed

common/httpclient.go

Lines changed: 100 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,71 @@ import (
1010
"sync"
1111
"sync/atomic"
1212

13-
"github.com/getlantern/flashlight/v7/sentry"
13+
"github.com/getlantern/dnstt"
1414
"github.com/getlantern/fronted"
1515
"github.com/getlantern/kindling"
16+
17+
"github.com/getlantern/flashlight/v7/sentry"
1618
)
1719

18-
var httpClient *http.Client
19-
var mutex = &sync.Mutex{}
20-
21-
// These are the domains we will access via kindling.
22-
var domains = []string{
23-
"api.iantem.io",
24-
"api.getiantem.org", // Still used on iOS
25-
"geo.getiantem.org", // Still used on iOS
26-
"config.getiantem.org", // Still used on iOS
27-
"df.iantem.io",
28-
"raw.githubusercontent.com",
29-
"media.githubusercontent.com",
30-
"objects.githubusercontent.com",
31-
"replica-r2.lantern.io",
32-
"replica-search.lantern.io",
33-
"update.getlantern.org",
34-
"globalconfig.flashlightproxy.com",
35-
"dogsdogs.xyz", // Used in replica
36-
"service.dogsdogs.xyz", // Used in replica
20+
var (
21+
httpClient *http.Client
22+
mutex = &sync.Mutex{}
23+
24+
// These are the domains we will access via kindling.
25+
domains = []string{
26+
"api.iantem.io",
27+
"api.getiantem.org", // Still used on iOS
28+
"geo.getiantem.org", // Still used on iOS
29+
"config.getiantem.org", // Still used on iOS
30+
"df.iantem.io",
31+
"raw.githubusercontent.com",
32+
"media.githubusercontent.com",
33+
"objects.githubusercontent.com",
34+
"replica-r2.lantern.io",
35+
"replica-search.lantern.io",
36+
"update.getlantern.org",
37+
"globalconfig.flashlightproxy.com",
38+
"dogsdogs.xyz", // Used in replica
39+
"service.dogsdogs.xyz", // Used in replica
40+
}
41+
42+
configDir atomic.Value
43+
dnsttConfig atomic.Value // Holds the DNSTTConfig
44+
45+
defaultDNSTTConfig = &DNSTTConfig{
46+
Domain: "t.iantem.io",
47+
PublicKey: "405eb9e22d806e3a0a8e667c6665a321c8a6a35fa680ed814716a66d7ad84977",
48+
DoHResolver: "https://cloudflare-dns.com/dns-query",
49+
DoTResolver: "",
50+
UTLSDistribution: "",
51+
}
52+
)
53+
54+
type DNSTTConfig struct {
55+
Domain string `yaml:"domain"` // DNS tunnel domain, e.g., "t.iantem.io"
56+
PublicKey string `yaml:"publicKey"` // DNSTT server public key
57+
DoHResolver string `yaml:"dohResolver,omitempty"`
58+
DoTResolver string `yaml:"dotResolver,omitempty"`
59+
UTLSDistribution string `yaml:"utlsDistribution,omitempty"`
3760
}
3861

39-
var configDir atomic.Value
62+
func init() {
63+
dnsttConfig.Store(defaultDNSTTConfig)
64+
}
4065

4166
// The config directory on some platforms, such as Android, can only be determined in native code, so we
4267
// need to set it externally.
4368
func SetConfigDir(dir string) {
4469
configDir.Store(dir)
4570
}
4671

72+
func SetDNSTTConfig(cfg *DNSTTConfig) {
73+
if cfg != nil {
74+
dnsttConfig.Store(cfg)
75+
}
76+
}
77+
4778
func GetHTTPClient() *http.Client {
4879
mutex.Lock()
4980
defer mutex.Unlock()
@@ -53,26 +84,25 @@ func GetHTTPClient() *http.Client {
5384

5485
var k kindling.Kindling
5586
ioWriter := log.AsDebugLogger().Writer()
87+
kOptions := []kindling.Option{
88+
kindling.WithPanicListener(sentry.PanicListener),
89+
kindling.WithLogWriter(ioWriter),
90+
kindling.WithProxyless(domains...),
91+
}
92+
5693
// Create new fronted instance.
5794
f, err := newFronted(ioWriter, sentry.PanicListener)
5895
if err != nil {
5996
log.Errorf("Failed to create fronted instance: %v", err)
60-
k = kindling.NewKindling(
61-
"flashlight",
62-
kindling.WithPanicListener(sentry.PanicListener),
63-
kindling.WithLogWriter(ioWriter),
64-
kindling.WithProxyless(domains...),
65-
)
6697
} else {
67-
// Set the client to the kindling client.
68-
k = kindling.NewKindling(
69-
"flashlight",
70-
kindling.WithPanicListener(sentry.PanicListener),
71-
kindling.WithLogWriter(ioWriter),
72-
kindling.WithDomainFronting(f),
73-
kindling.WithProxyless(domains...),
74-
)
98+
kOptions = append(kOptions, kindling.WithDomainFronting(f))
99+
}
100+
if d, err := newDNSTT(); err != nil {
101+
log.Errorf("Failed to create DNSTT: %v", err)
102+
} else {
103+
kOptions = append(kOptions, kindling.WithDNSTunnel(d))
75104
}
105+
k = kindling.NewKindling("flashlight", kOptions...)
76106
httpClient = k.NewHTTPClient()
77107
return httpClient
78108
}
@@ -110,3 +140,38 @@ func newFronted(logWriter io.Writer, panicListener func(string)) (fronted.Fronte
110140
fronted.WithConfigURL(configURL),
111141
), nil
112142
}
143+
144+
func (c *DNSTTConfig) Validate() error {
145+
if c.PublicKey == "" {
146+
return fmt.Errorf("publicKey is required")
147+
}
148+
if c.Domain == "" {
149+
return fmt.Errorf("domain is required")
150+
}
151+
if c.DoHResolver == "" && c.DoTResolver == "" {
152+
return fmt.Errorf("at least one of DoHResolver or DoTResolver must be specified")
153+
}
154+
return nil
155+
}
156+
157+
func newDNSTT() (dnstt.DNSTT, error) {
158+
cfg := dnsttConfig.Load().(*DNSTTConfig)
159+
if err := cfg.Validate(); err != nil {
160+
return nil, fmt.Errorf("invalid DNSTT configuration: %w", err)
161+
}
162+
163+
options := []dnstt.Option{
164+
dnstt.WithPublicKey(cfg.PublicKey),
165+
dnstt.WithTunnelDomain(cfg.Domain),
166+
}
167+
switch {
168+
case cfg.DoHResolver != "":
169+
options = append(options, dnstt.WithDoH(cfg.DoHResolver))
170+
case cfg.DoTResolver != "":
171+
options = append(options, dnstt.WithDoT(cfg.DoTResolver))
172+
}
173+
if cfg.UTLSDistribution != "" {
174+
options = append(options, dnstt.WithUTLSDistribution(cfg.UTLSDistribution))
175+
}
176+
return dnstt.NewDNSTT(options...)
177+
}

config/global.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package config
22

33
import (
4-
"github.com/getlantern/fronted"
54
"time"
65

6+
"github.com/getlantern/fronted"
7+
78
"github.com/getlantern/flashlight/v7/browsers/simbrowser"
9+
"github.com/getlantern/flashlight/v7/common"
810
"github.com/getlantern/flashlight/v7/domainrouting"
911
"github.com/getlantern/flashlight/v7/embeddedconfig"
1012
"github.com/getlantern/flashlight/v7/otel"
@@ -44,6 +46,9 @@ type Global struct {
4446
// TrustedCAs are trusted CAs for domain fronting domains only.
4547
TrustedCAs []*fronted.CA
4648

49+
// DNSTTConfig is the configuration for the DNS tunnel.
50+
DNSTTConfig *common.DNSTTConfig
51+
4752
// GlobalConfigPollInterval sets interval at which to poll for global config
4853
GlobalConfigPollInterval time.Duration
4954

embeddedconfig/global.yaml.tmpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,12 @@ otel:
428428
check_update: 1
429429
broflake_fronted_roundtrip: 800000
430430
new_broflake: 1000
431+
432+
{{with .dnstt}}
433+
dnsttconfig:
434+
domain: "{{.Domain}}"
435+
publicKey: "{{.PublicKey}}"
436+
dohResolver: "{{.DoHResolver}}"
437+
dotResolver: "{{.DoTResolver}}"
438+
utlsDistribution: "{{.UTLSDistribution}}"
439+
{{end}}

flashlight.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ func (f *Flashlight) onGlobalConfig(cfg *config.Global, src config.Source) {
396396
f.applyOtel(cfg)
397397
f.callbacks.onConfigUpdate(cfg, src)
398398
f.callbacks.onInit()
399+
400+
common.SetDNSTTConfig(cfg.DNSTTConfig)
399401
}
400402

401403
// EnabledFeatures gets all features enabled based on current conditions

genconfig/genconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:33bad90db6008dc8deaa61998b380c830bdb88b5f46bd59b88f2441da4a4336b
3-
size 34832016
2+
oid sha256:de1d3ec9511278c13bcdeda8ab1d57b74a20d64ac92e803c6282b1029e554638
3+
size 40599248

genconfig/genconfig.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/getlantern/tlsdialer/v3"
3030
"github.com/getlantern/yaml"
3131

32+
"github.com/getlantern/flashlight/v7/common"
3233
"github.com/getlantern/flashlight/v7/config"
3334
"github.com/getlantern/flashlight/v7/embeddedconfig"
3435

@@ -49,6 +50,7 @@ var (
4950
proxiedSitesDir = flag.String("proxiedsites", "proxiedsites", "Path to directory containing proxied site lists, which will be combined and proxied by Lantern")
5051
minFreq = flag.Float64("minfreq", 3.0, "Minimum frequency (percentage) for including CA cert in list of trusted certs, defaults to 3.0%")
5152
numberOfWorkers = flag.Int("numworkers", 50, "Number of worker threads")
53+
dnsttFile = flag.String("dnstt-file", "", "Path to yaml file containing DNSTT config")
5254

5355
enabledProviders stringsFlag // --enable-provider in init()
5456
masqueradesInFiles stringsFlag // --masquerades in init()
@@ -198,7 +200,16 @@ func (ss *stringsFlag) Set(value string) error {
198200
return nil
199201
}
200202

201-
func (c *ConfigGenerator) GenerateConfig(ctx context.Context, yamlTmpl string, masquerades []string, proxiedSites, blacklist filter, numberOfWorkers int, minFreq float64, minMasquerades, maxMasquerades int) ([]byte, error) {
203+
func (c *ConfigGenerator) GenerateConfig(
204+
ctx context.Context,
205+
yamlTmpl string,
206+
masquerades []string,
207+
proxiedSites, blacklist filter,
208+
numberOfWorkers int,
209+
minFreq float64,
210+
minMasquerades, maxMasquerades int,
211+
dnsttConfig *common.DNSTTConfig,
212+
) ([]byte, error) {
202213
if err := c.loadFtVersion(); err != nil {
203214
return nil, err
204215
}
@@ -209,7 +220,7 @@ func (c *ConfigGenerator) GenerateConfig(ctx context.Context, yamlTmpl string, m
209220
return nil, err
210221
}
211222

212-
model, err := c.buildModel("cloud.yaml", cas)
223+
model, err := c.buildModel("cloud.yaml", cas, dnsttConfig)
213224
if err != nil {
214225
return nil, fmt.Errorf("invalid configuration: %w", err)
215226
}
@@ -243,9 +254,24 @@ func main() {
243254
loadMasquerades()
244255
loadProxiedSitesList()
245256
loadBlacklist()
257+
dnsttCfg, err := loadDNSTTConfig()
258+
if err != nil {
259+
log.Errorf("Error loading DNSTT config: %s", err)
260+
}
246261

247262
yamlTmpl := string(embeddedconfig.GlobalTemplate)
248-
template, err := generator.GenerateConfig(context.Background(), yamlTmpl, masquerades, proxiedSites, blacklist, *numberOfWorkers, *minFreq, *minMasquerades, *maxMasquerades)
263+
template, err := generator.GenerateConfig(
264+
context.Background(),
265+
yamlTmpl,
266+
masquerades,
267+
proxiedSites,
268+
blacklist,
269+
*numberOfWorkers,
270+
*minFreq,
271+
*minMasquerades,
272+
*maxMasquerades,
273+
dnsttCfg,
274+
)
249275
if err != nil {
250276
log.Fatalf("Error generating configuration: %s", err)
251277
}
@@ -343,6 +369,24 @@ func loadBlacklist() {
343369
}
344370
}
345371

372+
func loadDNSTTConfig() (*common.DNSTTConfig, error) {
373+
if *dnsttFile == "" {
374+
return nil, nil
375+
}
376+
bytes, err := os.ReadFile(*dnsttFile)
377+
if err != nil {
378+
return nil, fmt.Errorf("Unable to read dnstt file at %s: %s", *dnsttFile, err)
379+
}
380+
var cfg common.DNSTTConfig
381+
if err := yaml.Unmarshal(bytes, &cfg); err != nil {
382+
return nil, fmt.Errorf("Unable to parse dnstt file at %s: %s", *dnsttFile, err)
383+
}
384+
if err := cfg.Validate(); err != nil {
385+
return nil, fmt.Errorf("Invalid DNSTT config: %s", err)
386+
}
387+
return &cfg, nil
388+
}
389+
346390
func loadTemplate(name string) string {
347391
bytes, err := os.ReadFile(name)
348392
if err != nil {
@@ -558,7 +602,7 @@ func (c *ConfigGenerator) doVetMasquerades(certPool *x509.CertPool, inCh chan *m
558602
c.wg.Done()
559603
}
560604

561-
func (c *ConfigGenerator) buildModel(configName string, cas map[string]*castat) (map[string]interface{}, error) {
605+
func (c *ConfigGenerator) buildModel(configName string, cas map[string]*castat, dnsttCfg *common.DNSTTConfig) (map[string]interface{}, error) {
562606
casList := make([]*castat, 0, len(cas))
563607
for _, ca := range cas {
564608
casList = append(casList, ca)
@@ -607,6 +651,7 @@ func (c *ConfigGenerator) buildModel(configName string, cas map[string]*castat)
607651
"providers": enabledProviders,
608652
"proxiedsites": ps,
609653
"ftVersion": c.ftVersion,
654+
"dnstt": dnsttCfg,
610655
}, nil
611656
}
612657

0 commit comments

Comments
 (0)