Skip to content

Commit b7fe02b

Browse files
authored
feat: create Smart Dialer (#185)
1 parent 171dbe3 commit b7fe02b

17 files changed

+1568
-23
lines changed

transport/happyeyeballs_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ func ExampleNewParallelHappyEyeballsResolveFunc() {
315315
dialer := HappyEyeballsStreamDialer{
316316
Dialer: FuncStreamDialer(func(ctx context.Context, addr string) (StreamConn, error) {
317317
ips = append(ips, netip.MustParseAddrPort(addr).Addr())
318+
// Add a slight delay to simulate a more real life ordering.
319+
time.Sleep(1 * time.Millisecond)
318320
return nil, errors.New("not implemented")
319321
}),
320322
Resolve: NewParallelHappyEyeballsResolveFunc(

x/examples/fetch-proxy/main.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ func main() {
3434
log.Fatal("Need to pass the URL to fetch in the command-line")
3535
}
3636

37-
proxy, err := mobileproxy.RunProxy("localhost:0", *transportFlag)
37+
dialer, err := mobileproxy.NewStreamDialerFromConfig(*transportFlag)
3838
if err != nil {
39-
log.Fatalf("Cmobileproxy start proxy: %v", err)
39+
log.Fatalf("NewStreamDialerFromConfig failed: %v", err)
40+
}
41+
proxy, err := mobileproxy.RunProxy("localhost:0", dialer)
42+
if err != nil {
43+
log.Fatalf("RunProxy failed: %v", err)
4044
}
4145

4246
httpClient := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(&url.URL{Scheme: "http", Host: proxy.Address()})}}

x/examples/smart-proxy/config.json

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
{
2+
"dns": [
3+
{"https": {"name": "2620:fe::fe"}, "//": "Quad9"},
4+
{"https": {"name": "9.9.9.9"}},
5+
{"https": {"name": "149.112.112.112"}},
6+
7+
{"https": {"name": "2001:4860:4860::8888"}, "//": "Google"},
8+
{"https": {"name": "8.8.8.8"}},
9+
{"https": {"name": "2001:4860:4860::8844"}},
10+
{"https": {"name": "8.8.4.4"}},
11+
12+
{"https": {"name": "2606:4700:4700::1111"}, "//": "Cloudflare"},
13+
{"https": {"name": "1.1.1.1"}},
14+
{"https": {"name": "2606:4700:4700::1001"}},
15+
{"https": {"name": "1.0.0.1"}},
16+
{"https": {"name": "cloudflare-dns.com.", "address": "cloudflare.net."}},
17+
18+
{"https": {"name": "2620:119:35::35"}, "//": "OpenDNS"},
19+
{"https": {"name": "208.67.220.220"}},
20+
{"https": {"name": "2620:119:53::53"}},
21+
{"https": {"name": "208.67.222.222"}},
22+
23+
{"https": {"name": "2001:67c:930::1"}, "//": "Wikimedia DNS"},
24+
{"https": {"name": "185.71.138.138"}},
25+
26+
{"https": {"name": "doh.dns.sb", "address": "cloudflare.net:443"}, "//": "DNS.SB"},
27+
28+
29+
{"tls": {"name": "2620:fe::fe"}, "//": "Quad9"},
30+
{"tls": {"name": "9.9.9.9"}},
31+
{"tls": {"name": "149.112.112.112"}},
32+
33+
{"tls": {"name": "2001:4860:4860::8888"}, "//": "Google"},
34+
{"tls": {"name": "8.8.8.8"}},
35+
{"tls": {"name": "2001:4860:4860::8844"}},
36+
{"tls": {"name": "8.8.4.4"}},
37+
38+
{"tls": {"name": "2606:4700:4700::1111"}, "//": "Cloudflare"},
39+
{"tls": {"name": "1.1.1.1"}},
40+
{"tls": {"name": "2606:4700:4700::1001"}},
41+
{"tls": {"name": "1.0.0.1"}},
42+
43+
{"tls": {"name": "2620:119:35::35"}, "//": "OpenDNS"},
44+
{"tls": {"name": "208.67.220.220"}},
45+
{"tls": {"name": "2620:119:53::53"}},
46+
{"tls": {"name": "208.67.222.222"}},
47+
48+
{"tls": {"name": "2001:67c:930::1"}, "//": "Wikimedia DNS"},
49+
{"tls": {"name": "185.71.138.138"}},
50+
51+
52+
{"tcp": {"address": "2620:fe::fe"}, "//": "Quad9"},
53+
{"tcp": {"address": "9.9.9.9"}},
54+
{"tcp": {"address": "149.112.112.112"}},
55+
{"tcp": {"address": "[2620:fe::fe]:9953"}},
56+
{"tcp": {"address": "9.9.9.9:9953"}},
57+
{"tcp": {"address": "149.112.112.112:9953"}},
58+
59+
{"tcp": {"address": "2001:4860:4860::8888"}, "//": "Google"},
60+
{"tcp": {"address": "8.8.8.8"}},
61+
{"tcp": {"address": "2001:4860:4860::8844"}},
62+
{"tcp": {"address": "8.8.4.4"}},
63+
64+
{"tcp": {"address": "2606:4700:4700::1111"}, "//": "Cloudflare"},
65+
{"tcp": {"address": "1.1.1.1"}},
66+
{"tcp": {"address": "2606:4700:4700::1001"}},
67+
{"tcp": {"address": "1.0.0.1"}},
68+
69+
{"tcp": {"address": "2620:119:35::35"}, "//": "OpenDNS"},
70+
{"tcp": {"address": "208.67.220.220"}},
71+
{"tcp": {"address": "2620:119:53::53"}},
72+
{"tcp": {"address": "208.67.222.222"}},
73+
{"tcp": {"address": "[2620:119:35::35]:443"}},
74+
{"tcp": {"address": "208.67.220.220:443"}},
75+
{"tcp": {"address": "[2620:119:35::35]:5353"}},
76+
{"tcp": {"address": "208.67.220.220:5353"}},
77+
78+
79+
{"udp": {"address": "2620:fe::fe"}, "//": "Quad9"},
80+
{"udp": {"address": "9.9.9.9"}},
81+
{"udp": {"address": "149.112.112.112"}},
82+
{"udp": {"address": "[2620:fe::fe]:9953"}},
83+
{"udp": {"address": "9.9.9.9:9953"}},
84+
{"udp": {"address": "149.112.112.112:9953"}},
85+
86+
{"udp": {"address": "2001:4860:4860::8888"}, "//": "Google"},
87+
{"udp": {"address": "8.8.8.8"}},
88+
{"udp": {"address": "2001:4860:4860::8844"}},
89+
{"udp": {"address": "8.8.4.4"}},
90+
91+
{"udp": {"address": "2606:4700:4700::1111"}, "//": "Cloudflare"},
92+
{"udp": {"address": "1.1.1.1"}},
93+
{"udp": {"address": "2606:4700:4700::1001"}},
94+
{"udp": {"address": "1.0.0.1"}},
95+
96+
{"udp": {"address": "2620:119:35::35"}, "//": "OpenDNS"},
97+
{"udp": {"address": "208.67.220.220"}},
98+
{"udp": {"address": "2620:119:53::53"}},
99+
{"udp": {"address": "208.67.222.222"}},
100+
{"udp": {"address": "[2620:119:35::35]:443"}},
101+
{"udp": {"address": "208.67.220.220:443"}},
102+
{"udp": {"address": "[2620:119:35::35]:5353"}},
103+
{"udp": {"address": "208.67.220.220:5353"}}
104+
],
105+
106+
"tls": [
107+
"",
108+
"split:1",
109+
"split:2",
110+
"tlsfrag:1"
111+
]
112+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"dns": [
3+
{"udp": {"address": "china.cn"}},
4+
{"udp": {"address": "ns1.tic.ir"}},
5+
{"tcp": {"address": "ns1.tic.ir"}},
6+
{"udp": {"address": "tmcell.tm"}},
7+
{"udp": {"address": "dns1.transtelecom.net."}},
8+
{"tls": {"name": "captive-portal.badssl.com", "address": "captive-portal.badssl.com:443"}},
9+
{"https": {"name": "mitm-software.badssl.com"}}
10+
],
11+
12+
"tls": [
13+
"",
14+
"split:1",
15+
"split:2",
16+
"split:5",
17+
"tlsfrag:1"
18+
]
19+
}

x/examples/smart-proxy/main.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright 2023 Jigsaw Operations LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"flag"
20+
"fmt"
21+
"io"
22+
"log"
23+
"net"
24+
"net/http"
25+
"os"
26+
"os/signal"
27+
"time"
28+
29+
"github.com/Jigsaw-Code/outline-sdk/transport"
30+
"github.com/Jigsaw-Code/outline-sdk/x/config"
31+
"github.com/Jigsaw-Code/outline-sdk/x/httpproxy"
32+
"github.com/Jigsaw-Code/outline-sdk/x/smart"
33+
)
34+
35+
var debugLog log.Logger = *log.New(io.Discard, "", 0)
36+
37+
type stringArrayFlagValue []string
38+
39+
func (v *stringArrayFlagValue) String() string {
40+
return fmt.Sprint(*v)
41+
}
42+
43+
func (v *stringArrayFlagValue) Set(value string) error {
44+
*v = append(*v, value)
45+
return nil
46+
}
47+
48+
func supportsHappyEyeballs(dialer transport.StreamDialer) bool {
49+
// Some proxy protocols, most notably Shadowsocks, can't communicate connection success.
50+
// Our shadowsocks.StreamDialer will return a connection successfully as long as it can
51+
// connect to the proxy server, regardless of whether it can connect to the target.
52+
// This breaks HappyEyeballs.
53+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
54+
conn, err := dialer.DialStream(ctx, "invalid:0")
55+
cancel()
56+
if conn != nil {
57+
conn.Close()
58+
}
59+
// If the dialer returns success on an invalid address, it doesn't support Happy Eyeballs.
60+
return err != nil
61+
}
62+
63+
func main() {
64+
verboseFlag := flag.Bool("v", false, "Enable debug output")
65+
addrFlag := flag.String("localAddr", "localhost:1080", "Local proxy address")
66+
configFlag := flag.String("config", "config.json", "Address of the config file")
67+
transportFlag := flag.String("transport", "", "The base transport for the connections")
68+
var domainsFlag stringArrayFlagValue
69+
flag.Var(&domainsFlag, "domain", "The test domains to find strategies.")
70+
71+
flag.Parse()
72+
if *verboseFlag {
73+
debugLog = *log.New(os.Stderr, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
74+
}
75+
76+
if len(domainsFlag) == 0 {
77+
log.Fatal("Must specify flag --domain")
78+
}
79+
80+
if *configFlag == "" {
81+
log.Fatal("Must specify flag --config")
82+
}
83+
84+
finderConfig, err := os.ReadFile(*configFlag)
85+
if err != nil {
86+
log.Fatalf("Could not read config: %v", err)
87+
}
88+
89+
packetDialer, err := config.NewPacketDialer(*transportFlag)
90+
if err != nil {
91+
log.Fatalf("Could not create packet dialer: %v", err)
92+
}
93+
streamDialer, err := config.NewStreamDialer(*transportFlag)
94+
if err != nil {
95+
log.Fatalf("Could not create stream dialer: %v", err)
96+
}
97+
if !supportsHappyEyeballs(streamDialer) {
98+
fmt.Println("⚠️ Warning: base transport is not compatible with Happy Eyeballs. Disabling IPv6.")
99+
innerDialer := streamDialer
100+
// Disable IPv6 if the dialer doesn't support HappyEyballs.
101+
streamDialer = transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) {
102+
host, _, err := net.SplitHostPort(addr)
103+
if err != nil {
104+
return nil, err
105+
}
106+
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
107+
return nil, fmt.Errorf("IPv6 not supported")
108+
}
109+
return innerDialer.DialStream(ctx, addr)
110+
})
111+
}
112+
finder := smart.StrategyFinder{
113+
LogWriter: debugLog.Writer(),
114+
TestTimeout: 5 * time.Second,
115+
StreamDialer: streamDialer,
116+
PacketDialer: packetDialer,
117+
}
118+
119+
fmt.Println("Finding strategy")
120+
startTime := time.Now()
121+
dialer, err := finder.NewDialer(context.Background(), domainsFlag, finderConfig)
122+
if err != nil {
123+
log.Fatalf("Failed to find dialer: %v", err)
124+
}
125+
fmt.Printf("Found strategy in %0.2fs\n", time.Since(startTime).Seconds())
126+
logDialer := transport.FuncStreamDialer(func(ctx context.Context, address string) (transport.StreamConn, error) {
127+
conn, err := dialer.DialStream(ctx, address)
128+
if err != nil {
129+
debugLog.Printf("Failed to dial %v: %v\n", address, err)
130+
}
131+
return conn, err
132+
})
133+
134+
listener, err := net.Listen("tcp", *addrFlag)
135+
if err != nil {
136+
log.Fatalf("Could not listen on address %v: %v", *addrFlag, err)
137+
}
138+
defer listener.Close()
139+
fmt.Printf("Proxy listening on %v\n", listener.Addr().String())
140+
141+
server := http.Server{
142+
Handler: httpproxy.NewProxyHandler(logDialer),
143+
ErrorLog: &debugLog,
144+
}
145+
go func() {
146+
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
147+
log.Fatalf("Error running web server: %v", err)
148+
}
149+
}()
150+
151+
// Wait for interrupt signal to stop the proxy.
152+
sig := make(chan os.Signal, 1)
153+
signal.Notify(sig, os.Interrupt)
154+
<-sig
155+
fmt.Println("Shutting down")
156+
// Gracefully shut down the server, with a 5s timeout.
157+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
158+
defer cancel()
159+
if err := server.Shutdown(ctx); err != nil {
160+
log.Fatalf("Failed to shutdown gracefully: %v", err)
161+
}
162+
}

x/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x
33
go 1.20
44

55
require (
6-
github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e
6+
github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854
77
github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b
88
github.com/stretchr/testify v1.8.2
99
github.com/vishvananda/netlink v1.1.0

x/go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e h1:56ZI48e68EYYb3m2slu3YJ6C+gWqh8v9bIWk+Bl9dfY=
2-
github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs=
1+
github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854 h1:SXp/tNjb70hpjF/MXAuLDkgCttlRA9qxLR7FCosGydg=
2+
github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs=
33
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
44
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

x/mobileproxy/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/out

0 commit comments

Comments
 (0)