Skip to content

Commit

Permalink
Create Smart Proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna committed Jan 19, 2024
1 parent 794f7d6 commit 0b02d88
Show file tree
Hide file tree
Showing 8 changed files with 1,377 additions and 2 deletions.
115 changes: 115 additions & 0 deletions x/examples/smart-proxy/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"dns": [
{"system": {}},

{"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",
"split:5",
"tlsfrag:1"
]
}
18 changes: 18 additions & 0 deletions x/examples/smart-proxy/config_broken.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"dns": [
{"udp": {"address": "china.cn"}},
{"udp": {"address": "ns1.tic.ir"}},
{"tcp": {"address": "ns1.tic.ir"}},
{"udp": {"address": "tmcell.tm"}},
{"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"
]
}
131 changes: 131 additions & 0 deletions x/examples/smart-proxy/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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 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)
}

finder := smart.StrategyFinder{
LogWriter: debugLog.Writer(),
TestTimeout: 5 * time.Second,
StreamDialer: streamDialer,
PacketDialer: packetDialer,
}

fmt.Println("Finding strategy")
dialer, err := finder.NewDialer(context.Background(), domainsFlag, finderConfig)
if err != nil {
log.Fatalf("Failed to find dialer: %v", err)
}
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.Print("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)
}
}
117 changes: 117 additions & 0 deletions x/internal/dnsextra/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 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 dnsextra

import (
"context"
"strings"
"time"

"github.com/Jigsaw-Code/outline-sdk/dns"
"golang.org/x/net/dns/dnsmessage"
)

// canonicalName returns the domain name in canonical form. A name in canonical
// form is lowercase and fully qualified. Only US-ASCII letters are affected. See
// Section 6.2 in RFC 4034.
func canonicalName(s string) string {
return strings.Map(func(r rune) rune {
if r >= 'A' && r <= 'Z' {
r += 'a' - 'A'
}
return r
}, s)
}

type cacheEntry struct {
key string
msg *dnsmessage.Message
expire time.Time
}

// cacheResolver is a very simple caching [RoundTripper].
// It doesn't use the response TTL and doesn't cache empty answers.
// It also doesn't dedup duplicate in-flight requests.
type cacheResolver struct {
resolver dns.Resolver
cache []cacheEntry
}

var _ dns.Resolver = (*cacheResolver)(nil)

func NewCacheResolver(resolver dns.Resolver, numEntries int) dns.Resolver {
return &cacheResolver{resolver: resolver, cache: make([]cacheEntry, numEntries)}
}

func (r *cacheResolver) removeExpired() {
now := time.Now()
last := 0
for _, entry := range r.cache {
if entry.expire.After(now) {
r.cache[last] = entry
last++
}
}
r.cache = r.cache[:last]
}

func (r *cacheResolver) moveToFront(index int) {
entry := r.cache[index]
copy(r.cache[1:], r.cache[:index])
r.cache[0] = entry
}

func makeCacheKey(q dnsmessage.Question) string {
domainKey := canonicalName(q.Name.String())
return strings.Join([]string{domainKey, q.Type.String(), q.Class.String()}, "|")
}

func (r *cacheResolver) searchCache(key string) *dnsmessage.Message {
for ei, entry := range r.cache {
if entry.key == key {
r.moveToFront(ei)
// TODO: update TTLs
// TODO: make names match
return entry.msg
}
}
return nil
}

func (r *cacheResolver) addToCache(key string, msg *dnsmessage.Message) {
newSize := len(r.cache) + 1
if newSize > cap(r.cache) {
newSize = cap(r.cache)
}
r.cache = r.cache[:newSize]
copy(r.cache[1:], r.cache[:newSize-1])
// TODO: copy and normalize names
r.cache[0] = cacheEntry{key: key, msg: msg, expire: time.Now().Add(60 * time.Second)}
}

func (r *cacheResolver) Query(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error) {
r.removeExpired()
cacheKey := makeCacheKey(q)
if msg := r.searchCache(cacheKey); msg != nil {
return msg, nil
}
msg, err := r.resolver.Query(ctx, q)
if err != nil {
// TODO: cache NXDOMAIN. See https://datatracker.ietf.org/doc/html/rfc2308.
return nil, err
}
r.addToCache(cacheKey, msg)
return msg, nil
}
Loading

0 comments on commit 0b02d88

Please sign in to comment.