Skip to content

Commit

Permalink
feat: create Smart Dialer (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna authored Feb 22, 2024
1 parent 171dbe3 commit b7fe02b
Show file tree
Hide file tree
Showing 17 changed files with 1,568 additions and 23 deletions.
2 changes: 2 additions & 0 deletions transport/happyeyeballs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ func ExampleNewParallelHappyEyeballsResolveFunc() {
dialer := HappyEyeballsStreamDialer{
Dialer: FuncStreamDialer(func(ctx context.Context, addr string) (StreamConn, error) {
ips = append(ips, netip.MustParseAddrPort(addr).Addr())
// Add a slight delay to simulate a more real life ordering.
time.Sleep(1 * time.Millisecond)
return nil, errors.New("not implemented")
}),
Resolve: NewParallelHappyEyeballsResolveFunc(
Expand Down
8 changes: 6 additions & 2 deletions x/examples/fetch-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ func main() {
log.Fatal("Need to pass the URL to fetch in the command-line")
}

proxy, err := mobileproxy.RunProxy("localhost:0", *transportFlag)
dialer, err := mobileproxy.NewStreamDialerFromConfig(*transportFlag)
if err != nil {
log.Fatalf("Cmobileproxy start proxy: %v", err)
log.Fatalf("NewStreamDialerFromConfig failed: %v", err)
}
proxy, err := mobileproxy.RunProxy("localhost:0", dialer)
if err != nil {
log.Fatalf("RunProxy failed: %v", err)
}

httpClient := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(&url.URL{Scheme: "http", Host: proxy.Address()})}}
Expand Down
112 changes: 112 additions & 0 deletions x/examples/smart-proxy/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
"dns": [
{"https": {"name": "2620:fe::fe"}, "//": "Quad9"},
{"https": {"name": "9.9.9.9"}},
{"https": {"name": "149.112.112.112"}},

{"https": {"name": "2001:4860:4860::8888"}, "//": "Google"},
{"https": {"name": "8.8.8.8"}},
{"https": {"name": "2001:4860:4860::8844"}},
{"https": {"name": "8.8.4.4"}},

{"https": {"name": "2606:4700:4700::1111"}, "//": "Cloudflare"},
{"https": {"name": "1.1.1.1"}},
{"https": {"name": "2606:4700:4700::1001"}},
{"https": {"name": "1.0.0.1"}},
{"https": {"name": "cloudflare-dns.com.", "address": "cloudflare.net."}},

{"https": {"name": "2620:119:35::35"}, "//": "OpenDNS"},
{"https": {"name": "208.67.220.220"}},
{"https": {"name": "2620:119:53::53"}},
{"https": {"name": "208.67.222.222"}},

{"https": {"name": "2001:67c:930::1"}, "//": "Wikimedia DNS"},
{"https": {"name": "185.71.138.138"}},

{"https": {"name": "doh.dns.sb", "address": "cloudflare.net:443"}, "//": "DNS.SB"},


{"tls": {"name": "2620:fe::fe"}, "//": "Quad9"},
{"tls": {"name": "9.9.9.9"}},
{"tls": {"name": "149.112.112.112"}},

{"tls": {"name": "2001:4860:4860::8888"}, "//": "Google"},
{"tls": {"name": "8.8.8.8"}},
{"tls": {"name": "2001:4860:4860::8844"}},
{"tls": {"name": "8.8.4.4"}},

{"tls": {"name": "2606:4700:4700::1111"}, "//": "Cloudflare"},
{"tls": {"name": "1.1.1.1"}},
{"tls": {"name": "2606:4700:4700::1001"}},
{"tls": {"name": "1.0.0.1"}},

{"tls": {"name": "2620:119:35::35"}, "//": "OpenDNS"},
{"tls": {"name": "208.67.220.220"}},
{"tls": {"name": "2620:119:53::53"}},
{"tls": {"name": "208.67.222.222"}},

{"tls": {"name": "2001:67c:930::1"}, "//": "Wikimedia DNS"},
{"tls": {"name": "185.71.138.138"}},


{"tcp": {"address": "2620:fe::fe"}, "//": "Quad9"},
{"tcp": {"address": "9.9.9.9"}},
{"tcp": {"address": "149.112.112.112"}},
{"tcp": {"address": "[2620:fe::fe]:9953"}},
{"tcp": {"address": "9.9.9.9:9953"}},
{"tcp": {"address": "149.112.112.112:9953"}},

{"tcp": {"address": "2001:4860:4860::8888"}, "//": "Google"},
{"tcp": {"address": "8.8.8.8"}},
{"tcp": {"address": "2001:4860:4860::8844"}},
{"tcp": {"address": "8.8.4.4"}},

{"tcp": {"address": "2606:4700:4700::1111"}, "//": "Cloudflare"},
{"tcp": {"address": "1.1.1.1"}},
{"tcp": {"address": "2606:4700:4700::1001"}},
{"tcp": {"address": "1.0.0.1"}},

{"tcp": {"address": "2620:119:35::35"}, "//": "OpenDNS"},
{"tcp": {"address": "208.67.220.220"}},
{"tcp": {"address": "2620:119:53::53"}},
{"tcp": {"address": "208.67.222.222"}},
{"tcp": {"address": "[2620:119:35::35]:443"}},
{"tcp": {"address": "208.67.220.220:443"}},
{"tcp": {"address": "[2620:119:35::35]:5353"}},
{"tcp": {"address": "208.67.220.220:5353"}},


{"udp": {"address": "2620:fe::fe"}, "//": "Quad9"},
{"udp": {"address": "9.9.9.9"}},
{"udp": {"address": "149.112.112.112"}},
{"udp": {"address": "[2620:fe::fe]:9953"}},
{"udp": {"address": "9.9.9.9:9953"}},
{"udp": {"address": "149.112.112.112:9953"}},

{"udp": {"address": "2001:4860:4860::8888"}, "//": "Google"},
{"udp": {"address": "8.8.8.8"}},
{"udp": {"address": "2001:4860:4860::8844"}},
{"udp": {"address": "8.8.4.4"}},

{"udp": {"address": "2606:4700:4700::1111"}, "//": "Cloudflare"},
{"udp": {"address": "1.1.1.1"}},
{"udp": {"address": "2606:4700:4700::1001"}},
{"udp": {"address": "1.0.0.1"}},

{"udp": {"address": "2620:119:35::35"}, "//": "OpenDNS"},
{"udp": {"address": "208.67.220.220"}},
{"udp": {"address": "2620:119:53::53"}},
{"udp": {"address": "208.67.222.222"}},
{"udp": {"address": "[2620:119:35::35]:443"}},
{"udp": {"address": "208.67.220.220:443"}},
{"udp": {"address": "[2620:119:35::35]:5353"}},
{"udp": {"address": "208.67.220.220:5353"}}
],

"tls": [
"",
"split:1",
"split:2",
"tlsfrag:1"
]
}
19 changes: 19 additions & 0 deletions x/examples/smart-proxy/config_broken.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"dns": [
{"udp": {"address": "china.cn"}},
{"udp": {"address": "ns1.tic.ir"}},
{"tcp": {"address": "ns1.tic.ir"}},
{"udp": {"address": "tmcell.tm"}},
{"udp": {"address": "dns1.transtelecom.net."}},
{"tls": {"name": "captive-portal.badssl.com", "address": "captive-portal.badssl.com:443"}},
{"https": {"name": "mitm-software.badssl.com"}}
],

"tls": [
"",
"split:1",
"split:2",
"split:5",
"tlsfrag:1"
]
}
162 changes: 162 additions & 0 deletions x/examples/smart-proxy/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"context"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/config"
"github.com/Jigsaw-Code/outline-sdk/x/httpproxy"
"github.com/Jigsaw-Code/outline-sdk/x/smart"
)

var debugLog log.Logger = *log.New(io.Discard, "", 0)

type stringArrayFlagValue []string

func (v *stringArrayFlagValue) String() string {
return fmt.Sprint(*v)
}

func (v *stringArrayFlagValue) Set(value string) error {
*v = append(*v, value)
return nil
}

func supportsHappyEyeballs(dialer transport.StreamDialer) bool {
// Some proxy protocols, most notably Shadowsocks, can't communicate connection success.
// Our shadowsocks.StreamDialer will return a connection successfully as long as it can
// connect to the proxy server, regardless of whether it can connect to the target.
// This breaks HappyEyeballs.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
conn, err := dialer.DialStream(ctx, "invalid:0")
cancel()
if conn != nil {
conn.Close()
}
// If the dialer returns success on an invalid address, it doesn't support Happy Eyeballs.
return err != nil
}

func main() {
verboseFlag := flag.Bool("v", false, "Enable debug output")
addrFlag := flag.String("localAddr", "localhost:1080", "Local proxy address")
configFlag := flag.String("config", "config.json", "Address of the config file")
transportFlag := flag.String("transport", "", "The base transport for the connections")
var domainsFlag stringArrayFlagValue
flag.Var(&domainsFlag, "domain", "The test domains to find strategies.")

flag.Parse()
if *verboseFlag {
debugLog = *log.New(os.Stderr, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
}

if len(domainsFlag) == 0 {
log.Fatal("Must specify flag --domain")
}

if *configFlag == "" {
log.Fatal("Must specify flag --config")
}

finderConfig, err := os.ReadFile(*configFlag)
if err != nil {
log.Fatalf("Could not read config: %v", err)
}

packetDialer, err := config.NewPacketDialer(*transportFlag)
if err != nil {
log.Fatalf("Could not create packet dialer: %v", err)
}
streamDialer, err := config.NewStreamDialer(*transportFlag)
if err != nil {
log.Fatalf("Could not create stream dialer: %v", err)
}
if !supportsHappyEyeballs(streamDialer) {
fmt.Println("⚠️ Warning: base transport is not compatible with Happy Eyeballs. Disabling IPv6.")
innerDialer := streamDialer
// Disable IPv6 if the dialer doesn't support HappyEyballs.
streamDialer = transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
return nil, fmt.Errorf("IPv6 not supported")
}
return innerDialer.DialStream(ctx, addr)
})
}
finder := smart.StrategyFinder{
LogWriter: debugLog.Writer(),
TestTimeout: 5 * time.Second,
StreamDialer: streamDialer,
PacketDialer: packetDialer,
}

fmt.Println("Finding strategy")
startTime := time.Now()
dialer, err := finder.NewDialer(context.Background(), domainsFlag, finderConfig)
if err != nil {
log.Fatalf("Failed to find dialer: %v", err)
}
fmt.Printf("Found strategy in %0.2fs\n", time.Since(startTime).Seconds())
logDialer := transport.FuncStreamDialer(func(ctx context.Context, address string) (transport.StreamConn, error) {
conn, err := dialer.DialStream(ctx, address)
if err != nil {
debugLog.Printf("Failed to dial %v: %v\n", address, err)
}
return conn, err
})

listener, err := net.Listen("tcp", *addrFlag)
if err != nil {
log.Fatalf("Could not listen on address %v: %v", *addrFlag, err)
}
defer listener.Close()
fmt.Printf("Proxy listening on %v\n", listener.Addr().String())

server := http.Server{
Handler: httpproxy.NewProxyHandler(logDialer),
ErrorLog: &debugLog,
}
go func() {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Fatalf("Error running web server: %v", err)
}
}()

// Wait for interrupt signal to stop the proxy.
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
fmt.Println("Shutting down")
// Gracefully shut down the server, with a 5s timeout.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Failed to shutdown gracefully: %v", err)
}
}
2 changes: 1 addition & 1 deletion x/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x
go 1.20

require (
github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e
github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854
github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b
github.com/stretchr/testify v1.8.2
github.com/vishvananda/netlink v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions x/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e h1:56ZI48e68EYYb3m2slu3YJ6C+gWqh8v9bIWk+Bl9dfY=
github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs=
github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854 h1:SXp/tNjb70hpjF/MXAuLDkgCttlRA9qxLR7FCosGydg=
github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
1 change: 1 addition & 0 deletions x/mobileproxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/out
Loading

0 comments on commit b7fe02b

Please sign in to comment.