From fd5c34dca18c7a668031fb207c5b461fa33dbcc0 Mon Sep 17 00:00:00 2001 From: wweir Date: Wed, 5 Feb 2020 17:51:34 +0800 Subject: [PATCH 1/6] Refactor to proxy router --- Makefile | 4 +- conf/conf.go | 185 ++++++++------ conf/conf_darwin.go | 26 ++ conf/conf_linux.go | 25 ++ conf/conf_other.go | 46 ---- conf/conf_windows.go | 43 ++-- conf/sower.toml | 29 ++- dns/dhcp_test.go | 22 -- dns/dns.go | 248 ++++++++----------- dns/level_string.go | 16 -- dns/util.go | 62 ++--- go.mod | 24 +- go.sum | 138 +++++------ {util => internal/http}/http_ping.go | 45 ++-- {dns => internal/net}/dhcp.go | 5 +- {util => internal/net}/pick_iface_other.go | 6 +- {util => internal/net}/pick_iface_windows.go | 6 +- internal/net/util.go | 9 + {proxy => internal}/socks5/socks5.go | 5 +- main.go | 103 +++++--- mux/parse.go | 42 ++++ {proxy/parser => mux}/sni.go | 2 +- proxy/client.go | 98 -------- proxy/http_proxy.go | 127 ---------- proxy/parser/doc.go | 12 - proxy/parser/parse.go | 163 ------------ proxy/parser/parse_test.go | 97 -------- proxy/server.go | 51 ---- proxy/shadow/cipher.go | 76 ------ proxy/shadow/doc.go | 5 - proxy/shadow/shadow.go | 111 --------- proxy/shadow/shadow_test.go | 22 -- proxy/shadow/typ_string.go | 16 -- proxy/transport/kcp.go | 115 --------- proxy/transport/quic.go | 137 ---------- proxy/transport/tcp.go | 58 ----- proxy/transport/util.go | 30 --- proxy/util.go | 53 ---- util/pick_iface_test.go | 23 -- util/util.go | 30 --- 40 files changed, 538 insertions(+), 1777 deletions(-) create mode 100644 conf/conf_darwin.go create mode 100644 conf/conf_linux.go delete mode 100644 conf/conf_other.go delete mode 100644 dns/dhcp_test.go delete mode 100644 dns/level_string.go rename {util => internal/http}/http_ping.go (80%) rename {dns => internal/net}/dhcp.go (95%) rename {util => internal/net}/pick_iface_other.go (78%) rename {util => internal/net}/pick_iface_windows.go (90%) create mode 100644 internal/net/util.go rename {proxy => internal}/socks5/socks5.go (96%) create mode 100644 mux/parse.go rename {proxy/parser => mux}/sni.go (99%) delete mode 100644 proxy/client.go delete mode 100644 proxy/http_proxy.go delete mode 100644 proxy/parser/doc.go delete mode 100644 proxy/parser/parse.go delete mode 100644 proxy/parser/parse_test.go delete mode 100644 proxy/server.go delete mode 100644 proxy/shadow/cipher.go delete mode 100644 proxy/shadow/doc.go delete mode 100644 proxy/shadow/shadow.go delete mode 100644 proxy/shadow/shadow_test.go delete mode 100644 proxy/shadow/typ_string.go delete mode 100644 proxy/transport/kcp.go delete mode 100644 proxy/transport/quic.go delete mode 100644 proxy/transport/tcp.go delete mode 100644 proxy/transport/util.go delete mode 100644 proxy/util.go delete mode 100644 util/pick_iface_test.go delete mode 100644 util/util.go diff --git a/Makefile b/Makefile index 67aaffe..d738977 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,8 @@ test: go test ./... build: go build -ldflags \ - "-X main.version=$(shell git describe --tags) \ - -X main.date=$(shell date +%Y-%m-%d)" + "-X conf.version=$(shell git describe --tags) \ + -X conf.date=$(shell date +%Y-%m-%d)" image: docker build -t sower -f .github/Dockerfile . diff --git a/conf/conf.go b/conf/conf.go index 97588ce..4442fa8 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -3,128 +3,153 @@ package conf import ( "flag" "os" - "strconv" + "sync" - "github.com/golang/glog" toml "github.com/pelletier/go-toml" "github.com/wweir/sower/util" + "github.com/wweir/utils/log" ) -// Conf define the config items -var Conf = struct { - ConfigFile string - NetType string `toml:"net_type"` - Cipher string `toml:"cipher"` - Password string `toml:"password"` +var ( + version, date string - ServerPort string `toml:"server_port"` - ServerAddr string `toml:"server_addr"` - HTTPProxy string `toml:"http_proxy"` + flushOnce = sync.Once{} + flushMu = sync.Mutex{} + flushCh = make(chan struct{}) - DNSServer string `toml:"dns_server"` - ClientIP string `toml:"client_ip"` - SuggestLevel string `toml:"suggest_level"` - ClearDNSCache string `toml:"clear_dns_cache"` + // Conf define the config items + Conf = struct { + ConfigFile string - BlockList []string `toml:"blocklist"` - WhiteList []string `toml:"whitelist"` - Suggestions []string `toml:"suggestions"` - Verbose int `toml:"verbose"` - VersionOnly bool `toml:"-"` -}{} + Upstream struct { + Socks5 string `toml:"socks5"` + DNS string `toml:"dns"` + } `toml:"upstream"` + + Downstream struct { + ServeIP string `toml:"serve_ip"` + HTTPProxy string `toml:"http_proxy"` + } `toml:"downstream"` + + Router struct { + FlushDNSCmd string `toml:"flush_dns_cmd"` + ProxyLevel int `toml:"proxy_level"` + + PortMapping map[string]string `toml:"port_mapping"` + ProxyList []string `toml:"proxy_list"` + DirectList []string `toml:"direct_list"` + DynamicList []string `toml:"dynamic_list"` + } `toml:"router"` + }{} +) func init() { - initArgs() - if Conf.VersionOnly { - return + flag.StringVar(&Conf.ConfigFile, "f", "", "config file, keep empty for dynamic detect proxy rule") + flag.StringVar(&Conf.Upstream.Socks5, "socks5", "127.0.0.1:1080", "upstream socks5 address") + flag.StringVar(&Conf.Upstream.DNS, "dns", "", "upstream dns ip, keep empty to dynamic detect") + flag.StringVar(&Conf.Downstream.ServeIP, "serve", "127.0.0.1", "serve on address") + flag.StringVar(&Conf.Downstream.HTTPProxy, "http_proxy", "", "serve http proxy, eg: 127.0.0.1:8080") + flag.IntVar(&Conf.Router.ProxyLevel, "level", 2, "dynamic proxy level: 0~4") + + Init() // execute platform init logic + if !flag.Parsed() { + flag.Parse() } - if _, err := os.Stat(Conf.ConfigFile); os.IsNotExist(err) { - glog.Warningln("no config file has been load:", Conf.ConfigFile) - return - } - for i := range refreshFns { - if action, err := refreshFns[i](); err != nil { - glog.Fatalln(action+":", err) + if _, err := os.Stat(Conf.ConfigFile); err == nil { + for i := range loadConfigFns { + if err := loadConfigFns[i].fn(); err != nil { + log.Fatalw("load config", "config", Conf.ConfigFile, "step", loadConfigFns[i].step, "err", err) + } } } - go addSuggestions() + log.Infow("start", "version", version, "date", date, "config", Conf) } // refreshFns will be executed while init and write new config -var refreshFns = []func() (string, error){ - func() (string, error) { - action := "load config" - f, err := os.OpenFile(Conf.ConfigFile, os.O_RDONLY, 0644) - if err != nil { - return action, err - } - defer f.Close() - - //safe refresh config - file := Conf.ConfigFile - if err = toml.NewDecoder(f).Decode(&Conf); err != nil { - return action, err - } - Conf.ConfigFile = file - - return action, flag.Set("v", strconv.Itoa(Conf.Verbose)) - }, - func() (string, error) { - action := "clear dns cache" - if Conf.ClearDNSCache != "" { - return action, execute(Conf.ClearDNSCache) - } - return action, nil - }, -} - -// AddRefreshFn add refreshh function for reload config -func AddRefreshFn(init bool, fn func() (string, error)) error { - if init { - if _, err := fn(); err != nil { - return err - } +var loadConfigFns = []struct { + step string + fn func() error +}{{"load_config", func() error { + f, err := os.OpenFile(Conf.ConfigFile, os.O_RDONLY, 0644) + if err != nil { + return err } + defer f.Close() - refreshFns = append(refreshFns, fn) + //safe refresh config + file := Conf.ConfigFile + if err = toml.NewDecoder(f).Decode(&Conf); err != nil { + return err + } + Conf.ConfigFile = file return nil + +}}, {"flush_dns", func() error { + if Conf.Router.FlushDNSCmd != "" { + return execute(Conf.Router.FlushDNSCmd) + } + return nil +}}} + +// AddReloadConfigHook add hook function for reload config +func AddReloadConfigHook(step string, fn func() error) { + loadConfigFns = append(loadConfigFns, struct { + step string + fn func() error + }{step, fn}) } -// SuggestCh add domain into suggestios -var SuggestCh = make(chan string) +// AddDynamic add new domain into dynamic list +func AddDynamic(domain string) { + flushMu.Lock() + Conf.Router.DynamicList = append(Conf.Router.DynamicList, domain) + Conf.Router.DynamicList = util.NewReverseSecSlice(Conf.Router.DynamicList).Sort().Uniq() + flushMu.Unlock() -// addSuggestions add new domain into suggest rules -func addSuggestions() { - for domain := range SuggestCh { - Conf.Suggestions = append(Conf.Suggestions, domain) - Conf.Suggestions = util.NewReverseSecSlice(Conf.Suggestions).Sort().Uniq() + flushOnce.Do(func() { + if Conf.ConfigFile != "" { + go flushConf() + } + }) - { // safe write + select { + case flushCh <- struct{}{}: + default: + } +} + +func flushConf() { + for range flushCh { + // safe write + if Conf.ConfigFile != "" { f, err := os.OpenFile(Conf.ConfigFile+"~", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { - glog.Errorln(err) + log.Errorw("flush config", "step", "flush", "err", err) continue } + flushMu.Lock() if err := toml.NewEncoder(f).ArraysWithOneElementPerLine(true).Encode(Conf); err != nil { - glog.Errorln(err) + log.Errorw("flush config", "step", "flush", "err", err) + flushMu.Unlock() f.Close() continue } + flushMu.Unlock() f.Close() if err = os.Rename(Conf.ConfigFile+"~", Conf.ConfigFile); err != nil { - glog.Errorln(err) + log.Errorw("flush config", "step", "flush", "err", err) continue } } // reload config - for i := range refreshFns { - if action, err := refreshFns[i](); err != nil { - glog.Errorln(action+":", err) + for i := range loadConfigFns { + if err := loadConfigFns[i].fn(); err != nil { + log.Errorw("flush config", "step", loadConfigFns[i].step, "err", err) } } } diff --git a/conf/conf_darwin.go b/conf/conf_darwin.go new file mode 100644 index 0000000..7ac14df --- /dev/null +++ b/conf/conf_darwin.go @@ -0,0 +1,26 @@ +// +build darwin + +package conf + +import ( + "context" + "flag" + "fmt" + "os/exec" + "time" +) + +func Init() { + flag.StringVar(&Conf.Router.FlushDNSCmd, "flush_dns", "pkill mDNSResponder || true", "flush dns command") +} + +func execute(cmd string) error { + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + defer cancel() + + out, err := exec.CommandContext(ctx, "sh", "-c", Conf.Router.FlushDNSCmd).CombinedOutput() + if err != nil { + return fmt.Errorf("cmd: %s, err: %s, output: %s", Conf.Router.FlushDNSCmd, err, out) + } + return nil +} diff --git a/conf/conf_linux.go b/conf/conf_linux.go new file mode 100644 index 0000000..c8e598d --- /dev/null +++ b/conf/conf_linux.go @@ -0,0 +1,25 @@ +// +build linux + +package conf + +import ( + "context" + "fmt" + "os/exec" + "time" +) + +func Init() { + flag.StringVar(&Conf.Router.FlushDNSCmd, "flush_dns", "", "flush dns command") +} + +func execute(cmd string) error { + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + defer cancel() + + out, err := exec.CommandContext(ctx, "sh", "-c", Conf.Router.FlushDNSCmd).CombinedOutput() + if err != nil { + return fmt.Errorf("cmd: %s, err: %s, output: %s", Conf.Router.FlushDNSCmd, err, out) + } + return nil +} diff --git a/conf/conf_other.go b/conf/conf_other.go deleted file mode 100644 index 5131eea..0000000 --- a/conf/conf_other.go +++ /dev/null @@ -1,46 +0,0 @@ -// +build !windows - -package conf - -import ( - "context" - "flag" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/wweir/sower/dns" - "github.com/wweir/sower/proxy/shadow" - "github.com/wweir/sower/proxy/transport" -) - -func initArgs() { - cfgFile, _ := filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), "sower.toml")) - flag.StringVar(&Conf.ConfigFile, "f", cfgFile, "config file location") - flag.StringVar(&Conf.NetType, "n", "TCP", "net type (socks5 client only): "+strings.Join(transport.ListTransports(), ",")) - flag.StringVar(&Conf.Cipher, "C", "AES_128_GCM", "cipher type: "+strings.Join(shadow.ListCiphers(), ",")) - flag.StringVar(&Conf.Password, "p", "12345678", "password") - flag.StringVar(&Conf.ServerPort, "P", "5533", "server mode listen port") - flag.StringVar(&Conf.ServerAddr, "s", "", "server IP (run in CLIENT mode if set)") - flag.StringVar(&Conf.HTTPProxy, "H", "", "http proxy listen addr") - flag.StringVar(&Conf.DNSServer, "d", "114.114.114.114", "client dns server") - flag.StringVar(&Conf.ClientIP, "c", "127.0.0.1", "client dns service redirect IP") - flag.StringVar(&Conf.SuggestLevel, "l", "SPEEDUP", "suggest level setting: "+strings.Join(dns.ListSuggestLevels(), ",")) - flag.BoolVar(&Conf.VersionOnly, "V", false, "print sower version") - - if !flag.Parsed() { - flag.Set("logtostderr", "true") - flag.Parse() - } -} - -func execute(cmd string) error { - ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) - defer cancel() - - out, err := exec.CommandContext(ctx, "sh", "-c", Conf.ClearDNSCache).CombinedOutput() - return errors.Wrapf(err, "cmd: %s, output: %s, error", Conf.ClearDNSCache, out) -} diff --git a/conf/conf_windows.go b/conf/conf_windows.go index d301727..04f25ed 100644 --- a/conf/conf_windows.go +++ b/conf/conf_windows.go @@ -13,8 +13,7 @@ import ( "syscall" "time" - "github.com/golang/glog" - "github.com/pkg/errors" + "github.com/wweir/utils/log" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" @@ -24,19 +23,15 @@ import ( const name = "sower" const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue -func initArgs() { - cfgFile, _ := filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), "sower.toml")) - flag.StringVar(&Conf.ConfigFile, "f", cfgFile, "config file location") - flag.BoolVar(&Conf.VersionOnly, "V", false, "print sower version") - install := flag.Bool("install", false, "install sower as a service") - uninstall := flag.Bool("uninstall", false, "uninstall sower from service list") - exePath,_:=filepath.Abs(os.Args[0]) +func Init() { + exePath, _ := filepath.Abs(os.Args[0]) + logFile := filepath.Join(filepath.Dir(exePath), name+".log") - if !flag.Parsed() { - os.Mkdir("log", 0755) - flag.Set("log_dir", filepath.Dir(os.Args[0])+"/log") - flag.Parse() - } + install := flag.Bool("i", false, "install sower as a service") + uninstall := flag.Bool("u", false, "uninstall sower from service list") + logFile := flag.String("log", logFile, name+" log file path") + flag.StringVar(&Conf.Router.FlushDNSCmd, "flush_dns", "ipconfig /flushdnss", "flush dns command") + flag.Parse() switch { case *install: @@ -46,7 +41,7 @@ func initArgs() { s.Close() return fmt.Errorf("service %s already exists", name) } - s, err = m.CreateService(name, exePath, mgr.Config{ + s, err = m.CreateService(name, exePath, mgr.Config{ DisplayName: "Sower Proxy", StartType: windows.SERVICE_AUTO_START, }) @@ -77,18 +72,18 @@ func initArgs() { default: os.Chdir(filepath.Dir(os.Args[0])) if active, err := svc.IsAnInteractiveSession(); err != nil { - glog.Exitf("failed to determine if we are running in an interactive session: %v", err) + log.Fatalf("failed to determine if we are running in an interactive session: %v", err) } else if !active { go func() { elog, err := eventlog.Open(name) if err != nil { - glog.Exitln(err) + log.Fatalw("install service", "err", err) } defer elog.Close() if err := svc.Run(name, &myservice{}); err != nil { elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err)) - glog.Exitln(err) + log.Fatalw("install service", "err", err) } elog.Info(1, fmt.Sprintf("winsvc.RunAsService: %s service stopped", name)) os.Exit(0) @@ -110,12 +105,12 @@ func serviceDo(fn func(*mgr.Service) error) { func mgrDo(fn func(m *mgr.Mgr) error) { m, err := mgr.Connect() if err != nil { - glog.Exitln(err) + log.Fatalw("install service", "err", err) } defer m.Disconnect() if err := fn(m); err != nil { - glog.Fatalln(err) + log.Fatalw("install service", "err", err) } } @@ -124,7 +119,7 @@ type myservice struct{} func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { elog, err := eventlog.Open(name) if err != nil { - glog.Errorln(err) + log.Errorw("install service", "err", err) return } defer elog.Close() @@ -174,6 +169,8 @@ func execute(cmd string) error { command := exec.CommandContext(ctx, cmds[0], cmds[1:]...) command.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - out, err := command.CombinedOutput() - return errors.Wrapf(err, "cmd: %s, output: %s, error", Conf.ClearDNSCache, out) + if out, err := command.CombinedOutput(); err != nil { + return fmt.Errorf("cmd: %s, output: %s, err: %w", Conf.ClearDNSCache, out, err) + } + return nil } diff --git a/conf/sower.toml b/conf/sower.toml index f5f2699..c07cd12 100644 --- a/conf/sower.toml +++ b/conf/sower.toml @@ -1,14 +1,18 @@ -net_type="TCP" # TCP, KCP, QUIC, SOCKS5(client only) -cipher="AES_128_GCM" # AES_128_GCM, AES_192_GCM, AES_256_GCM, CHACHA20_IETF_POLY1305, XCHACHA20_IETF_POLY1305 -password="12345678" -server_port="5533" -# server_addr="remote-server" # replce it to remote server -http_proxy=":8080" # eg: 192.168.0.2:8080 -dns_server="" # eg: 223.5.5.5:53, Keep empty for dynamic setting from net env -client_ip="127.0.0.1" # listen the IP, dns target is the IP -# clear_dns_cache="pkill mDNSResponder || true" # Windows: "ipconfig /flushdnss" -suggest_level="SPEEDUP" # DISABLE, BLOCK, SPEEDUP -blocklist=[ +[upstream] +socks5="127.0.0.1:1080" +dns="" # eg: 223.5.5.5, keep empty to get it from network environment + +[downstream] +serve_ip="127.0.0.1" +http_proxy="" # eg: :8080, 127.0.0.1:8080, keep empty to disable it + +[router.port_mapping] +# 2222="aaa.bbb.cc:22" + +[router] +flush_dns_cmd="" # Platform-related, keep empty to use default command +proxy_level=2 # dynamic detect level, (0~4), more bigger more harder to be proxy +proxy_list=[ "**.google.*", # google "**.goo.gl", "**.googleusercontent.com", @@ -31,7 +35,7 @@ blocklist=[ "*.githubusercontent.com", "*.github.*", ] -whitelist=[ +direct_list=[ "**.in-addr.arpa", "imap.*.*", "imap.*.*.*", @@ -48,4 +52,3 @@ whitelist=[ "**.163.com", "**.weiyun.com", ] -verbose=0 diff --git a/dns/dhcp_test.go b/dns/dhcp_test.go deleted file mode 100644 index 0b56b2b..0000000 --- a/dns/dhcp_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package dns - -import ( - "runtime" - "testing" -) - -func TestGetDefaultDNSServer(t *testing.T) { - switch runtime.GOOS { - case "windows": - case "darwin": - default: - t.Skip("skip for some enviroment not support dhcp and permission set") - return - } - - if got, err := GetDefaultDNSServer(); err != nil { - t.Errorf("GetDefaultDNSServer() return error: %s", err) - } else { - t.Logf("GetDefaultDNSServer() return IP: %v", got) - } -} diff --git a/dns/dns.go b/dns/dns.go index 7541715..59ea225 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -1,35 +1,37 @@ package dns +/* + * Deep integration with package conf: github.com/wweir/sower/conf + */ import ( "context" + "fmt" "net" "strings" + "sync" "sync/atomic" "time" - "github.com/golang/glog" "github.com/miekg/dns" - mem "github.com/wweir/mem-go" - "github.com/wweir/sower/util" + "github.com/wweir/sower/conf" + "github.com/wweir/sower/internal/http" + internal_net "github.com/wweir/sower/internal/net" + "github.com/wweir/sower/internal/socks5" + "github.com/wweir/utils/log" + "github.com/wweir/utils/mem" ) -const colon = byte(':') - -func StartDNS(dnsServer, listenIP string, suggestCh chan<- string, level string) { - ip := net.ParseIP(listenIP) - - suggest := &intelliSuggest{suggestCh, parseSuggestLevel(level), listenIP, time.Second} - mem.DefaultCache = mem.New(time.Hour) - - dhcpCh := make(chan struct{}) - if dnsServer != "" { - if _, _, err := net.SplitHostPort(dnsServer); err != nil { - dnsServer = net.JoinHostPort(dnsServer, "53") - } - } else { - go dynamicSetUpstreamDNS(listenIP, &dnsServer, dhcpCh) - dhcpCh <- struct{}{} +func ServeDNS() { + serveIP := net.ParseIP(conf.Conf.Downstream.ServeIP) + if conf.Conf.Downstream.ServeIP == "" || serveIP.String() != conf.Conf.Downstream.ServeIP { + log.Fatalw("invalid listen ip", "ip", conf.Conf.Downstream.ServeIP) } + dnsServer, err := PickUpstreamDNS(serveIP.String(), conf.Conf.Upstream.DNS) + if err != nil { + log.Fatalw("") + } + + d := &detect{proxy: conf.Conf.Upstream.Socks5} dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { // *Msg r has an TSIG record and it was validated @@ -44,162 +46,126 @@ func StartDNS(dnsServer, listenIP string, suggestCh chan<- string, level string) } domain := r.Question[0].Name - if idx := strings.IndexByte(domain, colon); idx > 0 { + if idx := strings.IndexByte(domain, ':'); idx > 0 { domain = domain[:idx] // trim port } - matchAndServe(w, r, domain, listenIP, dnsServer, dhcpCh, ip, suggest) + if err := matchAndServe(w, r, serveIP, d, domain, dnsServer); err != nil { + server, err := PickUpstreamDNS(serveIP.String(), dnsServer) + if err != nil { + log.Errorw("detect upstream dns fail", "err", err) + } else { + dnsServer = server + } + } }) - server := &dns.Server{Addr: net.JoinHostPort(listenIP, "53"), Net: "udp"} - glog.Fatalln(server.ListenAndServe()) + server := &dns.Server{Addr: ":53", Net: "udp"} + log.Fatalw("dns serve fail", "err", server.ListenAndServe()) } -func dynamicSetUpstreamDNS(listenIP string, dnsServer *string, dhcpCh <-chan struct{}) { - addr, _ := dns.ReverseAddr(listenIP) - msg := &dns.Msg{ - MsgHdr: dns.MsgHdr{ - Id: dns.Id(), - RecursionDesired: false, - }, - Question: []dns.Question{{ - Name: addr, - Qtype: dns.TypeA, - Qclass: dns.ClassINET, - }}, +func PickUpstreamDNS(listenIP string, dnsServer string) (string, error) { + if dnsServer == "" { + return internal_net.GetDefaultDNSServer() } - for { - <-dhcpCh - if _, err := dns.Exchange(msg, *dnsServer); err == nil { - continue - } - - host, err := GetDefaultDNSServer() - if err != nil { - glog.Errorln(err) - continue - } - - // atomic action - *dnsServer = net.JoinHostPort(host, "53") - glog.Infoln("set dns server to", host) + if _, port, err := net.SplitHostPort(dnsServer); err != nil { + return "", fmt.Errorf("parse upstream dns(%s) server fail: %w", dnsServer, err) + } else if port == "" { + return net.JoinHostPort(dnsServer, "53"), nil } + return dnsServer, nil } -func matchAndServe(w dns.ResponseWriter, r *dns.Msg, domain, listenIP, dnsServer string, - dhcpCh chan struct{}, ipNet net.IP, suggest *intelliSuggest) { - inWriteList := whiteList.Match(domain) - if !inWriteList && (blockList.Match(domain) || suggestList.Match(domain)) { - glog.V(2).Infof("match %s suss", domain) - w.WriteMsg(localA(r, domain, ipNet)) - return +const timeout = 200 * time.Millisecond + +func matchAndServe(w dns.ResponseWriter, r *dns.Msg, serveIP net.IP, d *detect, domain, dnsServer string) error { + if (!whiteList.Match(domain)) && + (blockList.Match(domain) || suggestList.Match(domain)) { + w.WriteMsg(localA(r, domain, serveIP)) + return nil } - go mem.Remember(suggest, domain) - ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + if err := mem.Remember(d, domain); err != nil { + panic(err) + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() msg, err := dns.ExchangeContext(ctx, r, dnsServer) - if err != nil { - if dhcpCh != nil { - select { - case dhcpCh <- struct{}{}: - default: - } - } - glog.V(1).Infof("get dns of %s from %s fail: %s", domain, dnsServer, err) - return - } else if msg == nil { // expose any response except nil - glog.V(1).Infof("get dns of %s from %s return empty", domain, dnsServer) - return + if err != nil || msg == nil { + return err } w.WriteMsg(msg) + return nil } -type intelliSuggest struct { - suggestCh chan<- string - level level - listenIP string - timeout time.Duration +type detect struct { + proxy string + port http.Port } -func (i *intelliSuggest) GetOne(key interface{}) (iface interface{}, e error) { - iface, e = struct{}{}, nil - if i.level == DISABLE { - return - } - - // kill deadloop, for ugly wildcard setting dns setting +func (d *detect) Get(key interface{}) error { + // break deadloop, for ugly wildcard setting dns setting domain := strings.TrimSuffix(key.(string), ".") if strings.Count(domain, ".") > 10 { - return + return nil } - ip, err := net.LookupIP(domain) - if err != nil || len(ip) == 0 { - glog.V(1).Infoln(domain, ip, err) - return + wg := sync.WaitGroup{} + httpScore, httpsScore := new(int32), new(int32) + for _, ping := range [...]detect{{"", http.HTTP}, {"", http.HTTPS}} { + wg.Add(1) + go func(ping detect) { + defer wg.Done() + + if err := ping.port.Ping(domain, timeout); err != nil { + return + } + + switch ping.port { + case http.HTTP: + if !atomic.CompareAndSwapInt32(httpScore, 0, 2) { + atomic.AddInt32(httpScore, 1) + } + case http.HTTPS: + if !atomic.CompareAndSwapInt32(httpsScore, 0, 2) { + atomic.AddInt32(httpScore, 1) + } + } + }(ping) } + for _, ping := range [...]detect{{d.proxy, http.HTTP}, {d.proxy, http.HTTPS}} { + wg.Add(1) + go func(ping detect) { + defer wg.Done() - var ( - pings = [...]struct { - viaAddr string - port util.Port - }{ - {ip[0].String(), util.HTTP}, - {i.listenIP, util.HTTP}, - {ip[0].String(), util.HTTPS}, - {i.listenIP, util.HTTPS}, - } - protos = [...]*int32{ - new(int32), /*HTTP*/ - new(int32), /*HTTPS*/ - } - score = new(int32) - ) - for idx := range pings { - go func(idx int) { - if err := util.HTTPPing(pings[idx].viaAddr, domain, pings[idx].port, i.timeout); err != nil { - // local ping fail - if pings[idx].viaAddr == i.listenIP { - atomic.AddInt32(score, -1) - glog.V(1).Infof("remote ping %s fail", domain) - } else { - atomic.AddInt32(score, 1) - glog.V(1).Infof("local ping %s fail", domain) - } - - // remote ping faster - } else if pings[idx].viaAddr == i.listenIP { - if atomic.CompareAndSwapInt32(protos[idx/2], 0, 1) && i.level == SPEEDUP { - atomic.AddInt32(score, 1) - } - glog.V(1).Infof("remote ping %s faster", domain) - + if conn, err := net.Dial("tcp", ping.proxy); err != nil { + return } else { - atomic.CompareAndSwapInt32(protos[idx/2], 0, 2) - return // score change trigger add suggestion - } - - // check all remote pings are faster - if atomic.LoadInt32(score) == int32(len(protos)) { - for i := range protos { - if atomic.LoadInt32(protos[i]) != 1 { - return - } + conn = socks5.ToSocks5(conn, domain, ping.port.String()) + if err := ping.port.PingWithConn(domain, conn, timeout); err != nil { + return } } - // 1. local fail and remote success - // 2. all remote pings are faster - if atomic.LoadInt32(score) >= int32(len(protos)) { - old := atomic.SwapInt32(score, -1) // avoid readd the suggestion - i.suggestCh <- domain - glog.Infof("suggested domain: %s with score: %d", domain, old) + switch ping.port { + case http.HTTP: + if !atomic.CompareAndSwapInt32(httpScore, 0, -2) { + atomic.AddInt32(httpScore, -1) + } + case http.HTTPS: + if !atomic.CompareAndSwapInt32(httpsScore, 0, -2) { + atomic.AddInt32(httpScore, -1) + } } - }(idx) + }(ping) } - return + + wg.Wait() + if int(*httpScore+*httpsScore) >= conf.Conf.Router.ProxyLevel { + conf.AddDynamic(domain) + } + return nil } diff --git a/dns/level_string.go b/dns/level_string.go deleted file mode 100644 index ba6f934..0000000 --- a/dns/level_string.go +++ /dev/null @@ -1,16 +0,0 @@ -// Code generated by "stringer -type=level util.go"; DO NOT EDIT. - -package dns - -import "strconv" - -const _level_name = "DISABLEBLOCKSPEEDUPlevelEnd" - -var _level_index = [...]uint8{0, 7, 12, 19, 27} - -func (i level) String() string { - if i < 0 || i >= level(len(_level_index)-1) { - return "level(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _level_name[_level_index[i]:_level_index[i+1]] -} diff --git a/dns/util.go b/dns/util.go index 0fa0ae1..42d50cd 100644 --- a/dns/util.go +++ b/dns/util.go @@ -3,30 +3,27 @@ package dns import ( "net" - "github.com/golang/glog" "github.com/miekg/dns" + "github.com/wweir/sower/conf" "github.com/wweir/sower/util" + "github.com/wweir/utils/log" ) -var ( - blockList *util.Node - suggestList *util.Node - whiteList *util.Node -) +var blockList *util.Node +var suggestList *util.Node +var whiteList *util.Node -// LoadRules init rules from config -func LoadRules(blocklist, suggestions, whitelist []string, host string) { - blockList = loadRules("block", blocklist) - suggestList = loadRules("suggest", suggestions) - whiteList = loadRules("white", whitelist) - whiteList.Add(host) - glog.V(1).Infoln("reloaded config") -} +func init() { + reloadFn := func() error { + whiteList = util.NewNodeFromRules(".", conf.Conf.Router.DirectList...) + blockList = util.NewNodeFromRules(".", conf.Conf.Router.ProxyList...) + suggestList = util.NewNodeFromRules(".", conf.Conf.Router.DynamicList...) + log.Infow("reload config rules") + return nil + } -func loadRules(name string, list []string) *util.Node { - rule := util.NewNodeFromRules(".", list...) - glog.V(3).Infof("load %s rule:\n%s", name, rule) - return rule + reloadFn() + conf.AddReloadConfigHook("reload rules", reloadFn) } func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg { @@ -45,32 +42,3 @@ func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg { } return m } - -//go:generate stringer -type=level $GOFILE -type level int32 - -const ( - DISABLE level = iota - BLOCK - SPEEDUP - levelEnd -) - -func ListSuggestLevels() []string { - list := make([]string, 0, int(levelEnd)) - for i := level(0); i < levelEnd; i++ { - list = append(list, i.String()) - } - return list -} - -func parseSuggestLevel(suggestLevel string) level { - for i := level(0); i < levelEnd; i++ { - if suggestLevel == i.String() { - return i - } - } - - glog.Exitln("invalid suggest level: " + suggestLevel) - return levelEnd -} diff --git a/go.mod b/go.mod index 2bc71ac..f0308dd 100644 --- a/go.mod +++ b/go.mod @@ -3,25 +3,11 @@ module github.com/wweir/sower go 1.13 require ( - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/guregu/null v3.4.0+incompatible // indirect - github.com/klauspost/cpuid v1.2.1 // indirect - github.com/klauspost/reedsolomon v1.9.2 // indirect github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 - github.com/lib/pq v1.2.0 // indirect github.com/libp2p/go-reuseport v0.0.1 - github.com/lucas-clemente/quic-go v0.12.0 - github.com/miekg/dns v1.1.18 - github.com/pelletier/go-toml v1.4.0 - github.com/pkg/errors v0.8.1 - github.com/satori/go.uuid v1.2.0 // indirect - github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect - github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect - github.com/tjfoc/gmsm v1.0.1 // indirect - github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f // indirect - github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296 - github.com/xtaci/kcp-go v5.4.10+incompatible - github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect - golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 - golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe + github.com/miekg/dns v1.1.27 + github.com/pelletier/go-toml v1.6.0 + github.com/pkg/errors v0.9.1 + github.com/wweir/utils v0.0.0-20200201061343-d44781e298da + golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 ) diff --git a/go.sum b/go.sum index 6b99481..caf112c 100644 --- a/go.sum +++ b/go.sum @@ -1,110 +1,98 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= -github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 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= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM= github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/reedsolomon v1.9.2 h1:E9CMS2Pqbv+C7tsrYad4YC9MfhnMVWhMRsTi7U0UB18= -github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/influxdata/influxdb v1.7.9/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk= github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/libp2p/go-reuseport v0.0.1 h1:7PhkfH73VXfPJYKQ6JwS5I/eVcoyYi9IMNGc6FWpFLw= github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA= -github.com/lucas-clemente/quic-go v0.12.0 h1:TRbvZ6F++sofeGbh+Z2IIyIOhl8KyGnYuA06g2yrHdI= -github.com/lucas-clemente/quic-go v0.12.0/go.mod h1:UXJJPE4RfFef/xPO5wQm0tITK8gNfqwTxjbE7s3Vb8s= -github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI= -github.com/marten-seemann/qtls v0.3.2 h1:O7awy4bHEzSX/K3h+fZig3/Vo03s/RxlxgsAk9sYamI= -github.com/marten-seemann/qtls v0.3.2/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= -github.com/miekg/dns v1.1.18 h1:S82KA03bsvMvziY41d0WitiplMCt8QhawbSQLtoqsdI= -github.com/miekg/dns v1.1.18/go.mod h1:WgzbA6oji13JREwiNsRDNfl7jYdPnmz+VEuLrA+/48M= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= -github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU= -github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= -github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b h1:mnG1fcsIB1d/3vbkBak2MM0u+vhGhlQwpeimUi7QncM= -github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= -github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU= -github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f h1:QatZ4lsJBY3x1+Imst9g95+vUl7m52dqM9Pi4aSMW8w= github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f/go.mod h1:BNLmYJ8oMJPIPpNx5968jCyUhwEU1XT3YsuOqtbo5qo= -github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296 h1:/HkUfg+ZMx/tNdnyJdVlhyv+xO3A7ZlpfL9nFLWLYcc= -github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296/go.mod h1:k7rjBGWoJ+JKwvfe8juAX0zgybjo/Yo3JGkca5f/06s= -github.com/xtaci/kcp-go v5.4.10+incompatible h1:FgH1ji3efEmRFaHEeyim1RYN4Q/c8BT8VqXMklBmh84= -github.com/xtaci/kcp-go v5.4.10+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= -github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= -github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +github.com/wweir/utils v0.0.0-20200201061343-d44781e298da h1:d4/Y71orUtfo1Z8AnKKCQWBqupjY9zZpCsoU3G5hssw= +github.com/wweir/utils v0.0.0-20200201061343-d44781e298da/go.mod h1:Nv4eBGkUJiHDPgVowJJlQNcJYgMfst6IkjaDThH2/yI= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/util/http_ping.go b/internal/http/http_ping.go similarity index 80% rename from util/http_ping.go rename to internal/http/http_ping.go index 5b8bf50..7184a21 100644 --- a/util/http_ping.go +++ b/internal/http/http_ping.go @@ -1,4 +1,4 @@ -package util +package http import ( "bytes" @@ -6,49 +6,46 @@ import ( "encoding/binary" "io" "net" + "strconv" "time" ) -// HTTPPing try connect to a http(s) server with domain though the http addr -func HTTPPing(viaHost, domain string, port Port, timeout time.Duration) (err error) { - conn, err := net.DialTimeout("tcp", port.JoinAddr(viaHost), timeout) +// Port ========================== +type Port uint16 + +const HTTP Port = 80 +const HTTPS Port = 443 + +// Ping try connect to a http(s) server with domain though the http addr +func (p Port) Ping(domain string, timeout time.Duration) error { + conn, err := net.DialTimeout("tcp", net.JoinHostPort(domain, p.String()), timeout) if err != nil { return err } defer conn.Close() + return p.PingWithConn(domain, conn, timeout) +} + +// PingWithConn try connect to a http(s) server with domain though the http addr +func (p Port) PingWithConn(domain string, conn net.Conn, timeout time.Duration) error { conn.SetDeadline(time.Now().Add(timeout)) - if _, err = conn.Write(port.PingMsg(domain)); err != nil { + if _, err := conn.Write(p.PingMsg(domain)); err != nil { return err } // err -> nil: read something succ // err -> io.EOF: no such domain or connection refused // err -> timeout: tcp package has been dropped - _, err = conn.Read(make([]byte, 1)) - if err == io.EOF && viaHost == domain { + _, err := conn.Read(make([]byte, 1)) + if err == nil || err == io.EOF { return nil } return err } -// Port ========================== -type Port uint16 - -const ( - HTTP Port = iota - HTTPS -) - -func (p Port) JoinAddr(addr string) string { - switch p { - case HTTP: - return addr + ":80" - case HTTPS: - return addr + ":443" - default: - panic("invalid port") - } +func (p Port) String() string { + return strconv.Itoa(int(p)) } func (p Port) PingMsg(domain string) []byte { diff --git a/dns/dhcp.go b/internal/net/dhcp.go similarity index 95% rename from dns/dhcp.go rename to internal/net/dhcp.go index bb80410..f2131aa 100644 --- a/dns/dhcp.go +++ b/internal/net/dhcp.go @@ -1,4 +1,4 @@ -package dns +package net import ( "math/rand" @@ -9,14 +9,13 @@ import ( "github.com/krolaw/dhcp4" "github.com/libp2p/go-reuseport" "github.com/pkg/errors" - "github.com/wweir/sower/util" ) var xid = make([]byte, 4) var broadcastAddr, _ = net.ResolveUDPAddr("udp", "255.255.255.255:67") func GetDefaultDNSServer() (string, error) { - iface, err := util.PickInterface() + iface, err := PickInternetInterface() if err != nil { return "", errors.Wrap(err, "pick interface") } diff --git a/util/pick_iface_other.go b/internal/net/pick_iface_other.go similarity index 78% rename from util/pick_iface_other.go rename to internal/net/pick_iface_other.go index a52d2a5..8b56e3d 100644 --- a/util/pick_iface_other.go +++ b/internal/net/pick_iface_other.go @@ -1,14 +1,14 @@ // +build !windows -package util +package net import ( "errors" "net" ) -// PickInterface pick the first active net interface -func PickInterface() (*Iface, error) { +// PickInternetInterface pick the first active net interface +func PickInternetInterface() (*Iface, error) { ifaces, err := net.Interfaces() if err != nil { return nil, err diff --git a/util/pick_iface_windows.go b/internal/net/pick_iface_windows.go similarity index 90% rename from util/pick_iface_windows.go rename to internal/net/pick_iface_windows.go index e38c5f8..2bcf8ea 100644 --- a/util/pick_iface_windows.go +++ b/internal/net/pick_iface_windows.go @@ -1,6 +1,6 @@ // +build windows -package util +package net import ( "bytes" @@ -11,8 +11,8 @@ import ( "unsafe" ) -// PickInterface pick the first active net interface -func PickInterface() (*Iface, error) { +// PickInternetInterface pick the first active net interface +func PickInternetInterface() (*Iface, error) { list, err := getAdapterList() if err != nil { return nil, err diff --git a/internal/net/util.go b/internal/net/util.go new file mode 100644 index 0000000..d2fdd8d --- /dev/null +++ b/internal/net/util.go @@ -0,0 +1,9 @@ +package net + +import "net" + +// Iface is net interface address info +type Iface struct { + net.HardwareAddr + net.IP +} diff --git a/proxy/socks5/socks5.go b/internal/socks5/socks5.go similarity index 96% rename from proxy/socks5/socks5.go rename to internal/socks5/socks5.go index ac0bc67..b346fc1 100644 --- a/proxy/socks5/socks5.go +++ b/internal/socks5/socks5.go @@ -2,11 +2,10 @@ package socks5 import ( "encoding/binary" + "fmt" "io" "net" "strconv" - - "github.com/pkg/errors" ) func ToSocks5(c net.Conn, domain, port string) net.Conn { @@ -75,7 +74,7 @@ func (c *conn) Write(b []byte) (n int, err error) { switch resp.REP { case 0x00: default: - return 0, errors.Errorf("socks5 handshake fail, return code: %d", resp.REP) + return 0, fmt.Errorf("socks5 handshake fail, return code: %d", resp.REP) } switch resp.ATYP { diff --git a/main.go b/main.go index 8525e6d..d4785a5 100644 --- a/main.go +++ b/main.go @@ -1,50 +1,89 @@ package main import ( - "encoding/json" - "fmt" + "io" "net" + "sync" + "sync/atomic" + "time" - "github.com/golang/glog" "github.com/wweir/sower/conf" "github.com/wweir/sower/dns" - "github.com/wweir/sower/proxy" - "github.com/wweir/sower/proxy/transport" + "github.com/wweir/sower/internal/socks5" + "github.com/wweir/sower/mux" + "github.com/wweir/utils/log" ) -var version, date string - func main() { - cfg := &conf.Conf - if cfg.VersionOnly { - config, _ := json.MarshalIndent(cfg, "", "\t") - fmt.Printf("Version:\n\t%s %s\nConfig:\n%s", version, date, config) - return - } - glog.Infof("Starting sower(%s %s): %v", version, date, cfg) + go dns.ServeDNS() - tran, err := transport.GetTransport(cfg.NetType) + go proxy(conf.Conf.Upstream.Socks5, conf.Conf.Downstream.ServeIP, "80", mux.ParseHTTP) + go proxy(conf.Conf.Upstream.Socks5, conf.Conf.Downstream.ServeIP, "443", mux.ParseHTTPS) + + for port, target := range conf.Conf.Router.PortMapping { + go proxy(conf.Conf.Upstream.Socks5, conf.Conf.Downstream.ServeIP, port, + func(conn net.Conn) (net.Conn, string, error) { + return conn, target, nil + }) + } + + select {} +} + +func proxy(socks5Addr, serveIP, port string, mux func(net.Conn) (net.Conn, string, error)) { + ln, err := net.Listen("tcp", net.JoinHostPort(serveIP, port)) if err != nil { - glog.Exitln(err) + log.Fatalw("listen", "ip", serveIP, "port", port, "err", err) } - if cfg.ServerAddr == "" { - proxy.StartServer(tran, cfg.ServerPort, cfg.Cipher, cfg.Password) - - } else { - conf.AddRefreshFn(true, func() (string, error) { - dns.LoadRules(cfg.BlockList, cfg.Suggestions, cfg.WhiteList, cfg.ServerAddr) - return "load rules", nil - }) - - isSocks5 := (cfg.NetType == "SOCKS5") - serverAddr := net.JoinHostPort(cfg.ServerAddr, cfg.ServerPort) - - if cfg.HTTPProxy != "" { - go proxy.StartHttpProxy(tran, isSocks5, serverAddr, cfg.Cipher, cfg.Password, cfg.HTTPProxy) + log.Infow("start proxy", "port", port) + for { + conn, err := ln.Accept() + if err != nil { + log.Fatalw("listen", "ip", serveIP, "port", port, "err", err) } - go dns.StartDNS(cfg.DNSServer, cfg.ClientIP, conf.SuggestCh, cfg.SuggestLevel) - proxy.StartClient(tran, isSocks5, serverAddr, cfg.Cipher, cfg.Password, cfg.ClientIP) + if conn, target, err := mux(conn); err != nil { + log.Errorw("mux parse", "target", target, "err", err) + continue + } else { + go relay(socks5Addr, target, conn) + } } } + +func relay(socks5Addr, target string, conn net.Conn) { + var socks5Conn net.Conn + if c, err := net.Dial("tcp", socks5Addr); err != nil { + log.Errorw("dial socks5", "addr", socks5Addr, "err", err) + return + } else if host, port, err := net.SplitHostPort(target); err != nil { + log.Errorw("dial socks5", "target", target, "err", err) + return + } else { + socks5Conn = socks5.ToSocks5(c, host, port) + } + + wg := &sync.WaitGroup{} + exitFlag := new(int32) + wg.Add(2) + go redirect(conn, socks5Conn, wg, exitFlag) + redirect(socks5Conn, conn, wg, exitFlag) + wg.Wait() +} + +func redirect(dst, src net.Conn, wg *sync.WaitGroup, exitFlag *int32) { + io.Copy(dst, src) + + if atomic.CompareAndSwapInt32(exitFlag, 0, 1) { + // wakeup blocked goroutine + now := time.Now() + src.SetDeadline(now) + dst.SetDeadline(now) + } else { + src.Close() + dst.Close() + } + + wg.Done() +} diff --git a/mux/parse.go b/mux/parse.go new file mode 100644 index 0000000..99dcc0f --- /dev/null +++ b/mux/parse.go @@ -0,0 +1,42 @@ +package mux + +import ( + "bufio" + "io" + "net" + "net/http" + "strings" + + "github.com/wweir/sower/util" +) + +func ParseHTTP(conn net.Conn) (net.Conn, string, error) { + teeConn := &util.TeeConn{Conn: conn} + teeConn.StartOrReset() + defer teeConn.Stop() + + b := bufio.NewReader(teeConn) + resp, err := http.ReadRequest(b) + if err != nil { + return teeConn, "", err + } + if strings.Contains(resp.Host, ":") { + return teeConn, resp.Host, nil + } + return teeConn, resp.Host + ":80", nil +} + +func ParseHTTPS(conn net.Conn) (net.Conn, string, error) { + teeConn := &util.TeeConn{Conn: conn} + teeConn.StartOrReset() + defer teeConn.Stop() + + host, _, err := extractSNI(io.Reader(teeConn)) + if err != nil { + return teeConn, "", err + } + if strings.Contains(host, ":") { + return teeConn, host, nil + } + return teeConn, host + ":443", nil +} diff --git a/proxy/parser/sni.go b/mux/sni.go similarity index 99% rename from proxy/parser/sni.go rename to mux/sni.go index aa665d1..687da76 100644 --- a/proxy/parser/sni.go +++ b/mux/sni.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package parser +package mux import ( "encoding/binary" diff --git a/proxy/client.go b/proxy/client.go deleted file mode 100644 index 6f814cc..0000000 --- a/proxy/client.go +++ /dev/null @@ -1,98 +0,0 @@ -package proxy - -import ( - "net" - - "github.com/golang/glog" - "github.com/wweir/sower/proxy/parser" - "github.com/wweir/sower/proxy/shadow" - "github.com/wweir/sower/proxy/socks5" - "github.com/wweir/sower/proxy/transport" -) - -func StartClient(tran transport.Transport, isSocks5 bool, server, cipher, password, listenIP string) { - conn80 := listenLocal(listenIP, "80") - conn443 := listenLocal(listenIP, "443") - var isHttp bool - var conn net.Conn - - glog.Infoln("Client started.") - for { - select { - case conn = <-conn80: - isHttp = true - case conn = <-conn443: - isHttp = false - } - - resolveAddr(&server) - glog.V(1).Infof("new conn from (%s) to (%s)", conn.RemoteAddr(), server) - - rc, err := tran.Dial(server) - if err != nil { - conn.Close() - glog.Errorln(err) - continue - } - - switch { - case isSocks5 && isHttp: - c, host, port, err := parser.ParseHttpAddr(conn) - if err != nil { - c.Close() - rc.Close() - glog.Errorln(err) - continue - } - - conn = c - rc = socks5.ToSocks5(rc, host, port) - - case isSocks5 && !isHttp: - c, host, err := parser.ParseHttpsHost(conn) - if err != nil { - c.Close() - rc.Close() - glog.Errorln(err) - continue - } - - conn = c - rc = socks5.ToSocks5(rc, host, "443") - - case !isSocks5 && isHttp: - rc = shadow.Shadow(rc, cipher, password) - rc = parser.NewHttpConn(rc) - - case !isSocks5 && !isHttp: - rc = shadow.Shadow(rc, cipher, password) - rc = parser.NewHttpsConn(rc, "443") - } - - go relay(conn, rc) - } -} - -func listenLocal(listenIP string, port string) <-chan net.Conn { - connCh := make(chan net.Conn, 10) - go func() { - ln, err := net.Listen("tcp", net.JoinHostPort(listenIP, port)) - if err != nil { - glog.Fatalln(err) - } - - for { - conn, err := ln.Accept() - if err != nil { - glog.Errorln("accept", listenIP+port, "fail:", err) - continue - } - - conn.(*net.TCPConn).SetKeepAlive(true) - connCh <- conn - } - }() - - glog.Infoln("listening port:", port) - return connCh -} diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go deleted file mode 100644 index 627e88d..0000000 --- a/proxy/http_proxy.go +++ /dev/null @@ -1,127 +0,0 @@ -package proxy - -import ( - "context" - "crypto/tls" - "io" - "net" - "net/http" - "net/url" - "time" - - "github.com/golang/glog" - "github.com/wweir/sower/proxy/parser" - "github.com/wweir/sower/proxy/shadow" - "github.com/wweir/sower/proxy/socks5" - "github.com/wweir/sower/proxy/transport" -) - -func StartHttpProxy(tran transport.Transport, isSocks5 bool, server, cipher, password, addr string) { - srv := &http.Server{ - Addr: addr, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - resolveAddr(&server) - - if r.Method == http.MethodConnect { - httpsProxy(w, r, tran, isSocks5, server, cipher, password) - } else { - httpProxy(w, r, tran, isSocks5, server, cipher, password) - } - }), - // Disable HTTP/2. - TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, - IdleTimeout: 90 * time.Second, - } - - glog.Fatalln(srv.ListenAndServe()) -} - -func httpProxy(w http.ResponseWriter, r *http.Request, - tran transport.Transport, isSocks5 bool, server, cipher, password string) { - - roundTripper := &http.Transport{ - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - - if isSocks5 { - roundTripper.Proxy = func(*http.Request) (*url.URL, error) { - return url.Parse("socks5://" + server) - } - - } else { - roundTripper.DialContext = func(context.Context, string, string) (net.Conn, error) { - conn, err := tran.Dial(server) - if err != nil { - return nil, err - } - - conn = shadow.Shadow(conn, cipher, password) - return parser.NewHttpConn(conn), nil - } - } - - resp, err := roundTripper.RoundTrip(r) - if err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - glog.Errorln("serve https proxy, get remote data:", err) - return - } - defer resp.Body.Close() - - for k, vs := range resp.Header { - for _, v := range vs { - w.Header().Add(k, v) - } - } - w.WriteHeader(resp.StatusCode) - io.Copy(w, resp.Body) -} - -func httpsProxy(w http.ResponseWriter, r *http.Request, - tran transport.Transport, isSocks5 bool, server, cipher, password string) { - - // local conn - conn, _, err := w.(http.Hijacker).Hijack() - if err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - return - } - conn.(*net.TCPConn).SetKeepAlive(true) - - if _, err := conn.Write([]byte(r.Proto + " 200 Connection established\r\n\r\n")); err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - conn.Close() - glog.Errorln("serve https proxy, write data fail:", err) - return - } - - // remote conn - rc, err := tran.Dial(server) - if err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - conn.Close() - glog.Errorln("serve https proxy, dial remote fail:", err) - return - } - - host, port, err := net.SplitHostPort(r.Host) - if err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - conn.Close() - glog.Errorln("serve https proxy, dial remote fail:", err) - return - } - - if isSocks5 { - rc = socks5.ToSocks5(rc, host, port) - - } else { - rc = shadow.Shadow(rc, cipher, password) - rc = parser.NewHttpsConn(rc, port) - } - - relay(rc, conn) -} diff --git a/proxy/parser/doc.go b/proxy/parser/doc.go deleted file mode 100644 index bf127df..0000000 --- a/proxy/parser/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package parser transter conn to be a parser conn -// -// init request payload: -// (1) + (2))(+Overhead) + (size+Overhead) -// data definition: -// 0x00(any): [size](1) + [addr:port] + content -// 0x01(http): content -// 0x02(https): [port](2) + content -// -// init response payload: -// ([status code](2) + (2))(+Overhead) + (size+Overhead) -package parser diff --git a/proxy/parser/parse.go b/proxy/parser/parse.go deleted file mode 100644 index c8e8d52..0000000 --- a/proxy/parser/parse.go +++ /dev/null @@ -1,163 +0,0 @@ -package parser - -import ( - "bufio" - "io" - "net" - "net/http" - "strconv" - "strings" - - "github.com/pkg/errors" - "github.com/wweir/sower/util" -) - -const ( - OTHER byte = iota - HTTP - HTTPS -) - -// Write Addr -type conn struct { - typ byte - domain string - port string - init bool - net.Conn -} - -func NewOtherConn(c net.Conn, domain, port string) net.Conn { - return &conn{ - typ: OTHER, - domain: domain, - port: port, - init: true, - Conn: c, - } -} -func NewHttpConn(c net.Conn) net.Conn { - return &conn{ - typ: HTTP, - init: true, - Conn: c, - } -} -func NewHttpsConn(c net.Conn, port string) net.Conn { - return &conn{ - typ: HTTPS, - port: port, - init: true, - Conn: c, - } -} - -func (c *conn) Write(b []byte) (n int, err error) { - if c.init { - var pkg []byte - var prefixLen int - switch c.typ { - case OTHER: - // type + domain + ':' + port + data - prefixLen = 1 + len(c.domain) + 1 + len(c.port) - pkg = make([]byte, 0, prefixLen+len(b)) - pkg = append(pkg, OTHER) - pkg = append(pkg, byte(len(c.domain)+1+len(c.port))) - pkg = append(pkg, []byte(c.domain+":"+c.port)...) - - case HTTP: - // type + data - prefixLen = 1 - pkg = make([]byte, 0, prefixLen+len(b)) - pkg = append(pkg, HTTP) - - case HTTPS: - // type + port + data - prefixLen = 1 + 2 - pkg = make([]byte, 0, prefixLen+len(b)) - pkg = append(pkg, HTTPS) - port, _ := strconv.Atoi(c.port) - pkg = append(pkg, byte(port>>8), byte(port)) - } - - c.init = false - n, err := c.Conn.Write(append(pkg, b...)) - // n should larger than prefix length, if not, err is not nil - return n - prefixLen, err - } - - return c.Conn.Write(b) -} - -// Read Addr -func ParseAddr(conn net.Conn) (net.Conn, string, string, error) { - buf := make([]byte, 1) - if _, err := io.ReadFull(conn, buf); err != nil { - return conn, "", "", err - } - - switch buf[0] { - case OTHER: - if _, err := io.ReadFull(conn, buf); err != nil { - return conn, "", "", err - } - buf = make([]byte, int(buf[0])) - if _, err := io.ReadFull(conn, buf); err != nil { - return conn, "", "", err - } - - addr := string(buf) - if idx := strings.LastIndex(addr, ":"); idx != -1 { - return conn, addr[:idx], addr[idx+1:], nil - } - return conn, "", "", errors.New("invalid payload") - - case HTTP: - return ParseHttpAddr(conn) - - case HTTPS: - buf = make([]byte, 2) - if _, err := io.ReadFull(conn, buf); err != nil { - return conn, "", "", err - } - port := strconv.Itoa(int(buf[0])<<8 + int(buf[1])) - - conn, domain, err := ParseHttpsHost(conn) - return conn, domain, port, err - - default: - return conn, "", "", errors.Errorf("not supported type (%v)", buf[0]) - } -} - -func ParseHttpAddr(conn net.Conn) (net.Conn, string, string, error) { - teeConn := &util.TeeConn{Conn: conn} - teeConn.StartOrReset() - defer teeConn.Stop() - - b := bufio.NewReader(teeConn) - resp, err := http.ReadRequest(b) - if err != nil { - return teeConn, "", "", err - } - - if idx := strings.LastIndex(resp.Host, ":"); idx != -1 { - return teeConn, resp.Host[:idx], resp.Host[idx+1:], nil - } - return teeConn, resp.Host, "80", nil -} - -func ParseHttpsHost(conn net.Conn) (net.Conn, string, error) { - teeConn := &util.TeeConn{Conn: conn} - teeConn.StartOrReset() - defer teeConn.Stop() - - domain, _, err := extractSNI(teeConn) - if err != nil { - return teeConn, "", err - } else if domain == "" { - return teeConn, "", errors.New("ClientHello did not present an SNI extension") - } - - return teeConn, domain, nil -} diff --git a/proxy/parser/parse_test.go b/proxy/parser/parse_test.go deleted file mode 100644 index add722a..0000000 --- a/proxy/parser/parse_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package parser - -import ( - "bufio" - "bytes" - "io/ioutil" - "net" - "net/http" - "testing" - - "github.com/wweir/sower/proxy/shadow" - "github.com/wweir/sower/util" -) - -func TestParseAddr1(t *testing.T) { - c1, c2 := net.Pipe() - - go func() { - c1 = NewHttpConn(c1) - req, _ := http.NewRequest("GET", "http://wweir.cc", bytes.NewReader([]byte{1, 2, 3})) - req.Write(c1) - }() - - c2, host, port, err := ParseAddr(c2) - - if err != nil || host != "wweir.cc" || port != "80" { - t.Error(err, host, port) - } - - req, err := http.ReadRequest(bufio.NewReader(c2)) - if err != nil { - t.Error(err) - } - - data, err := ioutil.ReadAll(req.Body) - if err != nil || len(data) != 3 || data[0] != 1 { - t.Error(err, data) - } -} - -func TestParseAddr2(t *testing.T) { - c1, c2 := net.Pipe() - - go func() { - c1 = NewHttpsConn(c1, "443") - c1.Write(util.HTTPS.PingMsg("wweir.cc")) - }() - - _, host, port, err := ParseAddr(c2) - - if err != nil || host != "wweir.cc" || port != "443" { - t.Error(err, host, port) - } -} - -func TestParseAddr3(t *testing.T) { - c1, c2 := net.Pipe() - - go func() { - c1 = NewOtherConn(c1, "wweir.cc", "1080") - c1.Write(util.HTTPS.PingMsg("wweir.cc")) - }() - - _, host, port, err := ParseAddr(c2) - - if err != nil || host != "wweir.cc" || port != "1080" { - t.Error(err, host, port) - } -} - -func TestParseAddr4(t *testing.T) { - c1, c2 := net.Pipe() - - go func() { - c1 = shadow.Shadow(c1, "AES_128_GCM", "12345678") - c1 = NewHttpConn(c1) - req, _ := http.NewRequest("GET", "http://wweir.cc", bytes.NewReader([]byte{1, 2, 3})) - req.Write(c1) - }() - - c2 = shadow.Shadow(c2, "AES_128_GCM", "12345678") - c2, host, port, err := ParseAddr(c2) - - if err != nil || host != "wweir.cc" || port != "80" { - t.Error(err, host, port) - } - - req, err := http.ReadRequest(bufio.NewReader(c2)) - if err != nil { - t.Error(err) - } - - data, err := ioutil.ReadAll(req.Body) - if err != nil || len(data) != 3 || data[0] != 1 { - t.Error(err, data) - } -} diff --git a/proxy/server.go b/proxy/server.go deleted file mode 100644 index 99da7a6..0000000 --- a/proxy/server.go +++ /dev/null @@ -1,51 +0,0 @@ -package proxy - -import ( - "net" - "strings" - - "github.com/golang/glog" - "github.com/wweir/sower/proxy/parser" - "github.com/wweir/sower/proxy/shadow" - "github.com/wweir/sower/proxy/transport" -) - -func StartServer(tran transport.Transport, port, cipher, password string) { - if port == "" { - glog.Fatalln("port must set") - } - if !strings.HasPrefix(port, ":") { - port = ":" + port - } - - connCh, err := tran.Listen(port) - if err != nil { - glog.Fatalf("listen %v fail: %s", port, err) - } - - glog.Infoln("Server started.") - for { - go handle(<-connCh, cipher, password) - } -} - -func handle(conn net.Conn, cipher, password string) { - conn = shadow.Shadow(conn, cipher, password) - conn, host, port, err := parser.ParseAddr(conn) - if err != nil { - conn.Close() - glog.Warningln(err) - return - } - glog.V(1).Infof("new conn from %s to %s:%s", conn.RemoteAddr(), host, port) - - rc, err := net.Dial("tcp", net.JoinHostPort(host, port)) - if err != nil { - conn.Close() - glog.Warningln(err) - return - } - rc.(*net.TCPConn).SetKeepAlive(true) - - relay(rc, conn) -} diff --git a/proxy/shadow/cipher.go b/proxy/shadow/cipher.go deleted file mode 100644 index c6af813..0000000 --- a/proxy/shadow/cipher.go +++ /dev/null @@ -1,76 +0,0 @@ -package shadow - -import ( - "crypto/aes" - "crypto/cipher" - - "github.com/pkg/errors" - "golang.org/x/crypto/chacha20poly1305" -) - -//go:generate stringer -type=typ $GOFILE -type typ int - -const ( - AES_128_GCM typ = iota - AES_192_GCM - AES_256_GCM - CHACHA20_IETF_POLY1305 - XCHACHA20_IETF_POLY1305 - cipherEnd -) - -func ListCiphers() []string { - list := make([]string, 0, int(cipherEnd)) - for i := typ(0); i < cipherEnd; i++ { - list = append(list, i.String()) - } - return list -} - -func pickCipher(typ, password string) (cipher.AEAD, error) { - var blockSize int - switch typ { - case AES_128_GCM.String(): - blockSize = 16 - case AES_192_GCM.String(): - blockSize = 24 - case AES_256_GCM.String(): - blockSize = 32 - - case CHACHA20_IETF_POLY1305.String(): - return chacha20poly1305.New(genKey(password, 256)) - case XCHACHA20_IETF_POLY1305.String(): - return chacha20poly1305.NewX(genKey(password, 256)) - - default: - return nil, errors.New("do not support cipher type: " + typ) - } - - // aes gcm - block, err := aes.NewCipher(genKey(password, blockSize)) - if err != nil { - return nil, errors.Wrap(err, "password") - } - - aead, err := cipher.NewGCM(block) - if err != nil { - return nil, errors.Wrap(err, AES_128_GCM.String()) - } - return aead, nil -} - -func genKey(filler string, size int) []byte { - res := make([]byte, size) - if filler == "" { - panic("password should not be empty") - } - - fillerByte := []byte(filler) - length := len(fillerByte) - for i := 0; ; i++ { - if copy(res[i*length:], fillerByte) != length { - return res - } - } -} diff --git a/proxy/shadow/doc.go b/proxy/shadow/doc.go deleted file mode 100644 index 9bc17fe..0000000 --- a/proxy/shadow/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package shadow transter conn to be a crypto conn -// support aead mode only -// data payload: -// (2+Overhead) + (size+Overhead) -package shadow diff --git a/proxy/shadow/shadow.go b/proxy/shadow/shadow.go deleted file mode 100644 index 510da42..0000000 --- a/proxy/shadow/shadow.go +++ /dev/null @@ -1,111 +0,0 @@ -package shadow - -import ( - "crypto/cipher" - "encoding/binary" - "io" - "math/rand" - "net" -) - -const MAX_SIZE = 0xFFFF - -type conn struct { - maxSize int - aead cipher.AEAD - encryptNonce func() []byte - decryptNonce func() []byte - writeBuf []byte - readBuf []byte - readOffset int - net.Conn -} - -func (c *conn) Read(b []byte) (n int, err error) { - // read from buffer - if c.readOffset != 0 { - dataSize := len(c.readBuf) - c.aead.Overhead() - n = copy(b, c.readBuf[c.readOffset:dataSize]) - c.readOffset += n - - if c.readOffset == dataSize { - c.readOffset = 0 - } - return - } - - // read from conn - dataSize := 0 - { //read data size - c.readBuf = make([]byte, 2+c.aead.Overhead()) - if _, err = io.ReadFull(c.Conn, c.readBuf); err != nil { - return - } - if _, err = c.aead.Open(c.readBuf[:0], c.decryptNonce(), c.readBuf, nil); err != nil { - return - } - dataSize = int(c.readBuf[0])<<8 + int(c.readBuf[1]) - } - { // read data - c.readBuf = make([]byte, dataSize+c.aead.Overhead()) - if _, err = io.ReadFull(c.Conn, c.readBuf); err != nil { - return - } - if _, err = c.aead.Open(c.readBuf[:0], c.decryptNonce(), c.readBuf, nil); err != nil { - return - } - } - - // buffer extra data - if n = copy(b, c.readBuf[:dataSize]); n < dataSize { - c.readOffset = n - } - return -} - -func (c *conn) Write(b []byte) (n int, err error) { - bLen := len(b) - dataSize := MAX_SIZE - (2 + c.aead.Overhead()) - c.aead.Overhead() - if bLen < c.maxSize { - dataSize = bLen - } - - // BigEndian - c.writeBuf[0], c.writeBuf[1] = byte(dataSize>>8), byte(dataSize) - - c.aead.Seal(c.writeBuf[:0], c.encryptNonce(), c.writeBuf[:2], nil) - c.aead.Seal(c.writeBuf[:2+c.aead.Overhead()], c.encryptNonce(), b[:dataSize], nil) - - _, err = c.Conn.Write(c.writeBuf[:dataSize+(2+c.aead.Overhead())+c.aead.Overhead()]) - if err != nil { - return 0, err - } - return dataSize, err -} - -func Shadow(c net.Conn, cipher, password string) net.Conn { - aead, err := pickCipher(cipher, password) - if err != nil { - panic(err) - } - - return &conn{ - maxSize: MAX_SIZE - (2 - aead.Overhead()) - aead.Overhead(), - aead: aead, - encryptNonce: newNonce(password, aead.NonceSize()), - decryptNonce: newNonce(password, aead.NonceSize()), - writeBuf: make([]byte, 0xFFFF), - Conn: c, - } -} - -func newNonce(password string, size int) func() []byte { - num, _ := binary.Varint([]byte(password)) - rnd := rand.New(rand.NewSource(num)) - - buf := make([]byte, size) - return func() []byte { - rnd.Read(buf) - return buf - } -} diff --git a/proxy/shadow/shadow_test.go b/proxy/shadow/shadow_test.go deleted file mode 100644 index 4ddb953..0000000 --- a/proxy/shadow/shadow_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package shadow - -import ( - "net" - "testing" -) - -func TestShadow(t *testing.T) { - c1, c2 := net.Pipe() - - go func() { - conn := Shadow(c1, "AES_128_GCM", "12345678") - conn.Write([]byte{1, 2}) - }() - - conn := Shadow(c2, "AES_128_GCM", "12345678") - buf := make([]byte, 3) - n, _ := conn.Read(buf) - if n!=2|| buf[0] != 1 || buf[1] != 2 { - t.Error(buf) - } -} diff --git a/proxy/shadow/typ_string.go b/proxy/shadow/typ_string.go deleted file mode 100644 index d6e344a..0000000 --- a/proxy/shadow/typ_string.go +++ /dev/null @@ -1,16 +0,0 @@ -// Code generated by "stringer -type=typ cipher.go"; DO NOT EDIT. - -package shadow - -import "strconv" - -const _typ_name = "AES_128_GCMAES_192_GCMAES_256_GCMCHACHA20_IETF_POLY1305XCHACHA20_IETF_POLY1305cipherEnd" - -var _typ_index = [...]uint8{0, 11, 22, 33, 55, 78, 87} - -func (i typ) String() string { - if i < 0 || i >= typ(len(_typ_index)-1) { - return "typ(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _typ_name[_typ_index[i]:_typ_index[i+1]] -} diff --git a/proxy/transport/kcp.go b/proxy/transport/kcp.go deleted file mode 100644 index 301bcd5..0000000 --- a/proxy/transport/kcp.go +++ /dev/null @@ -1,115 +0,0 @@ -package transport - -import ( - "net" - - "github.com/golang/glog" - "github.com/pkg/errors" - kcp "github.com/xtaci/kcp-go" -) - -type kcpTran struct { - client - server -} -type client struct { - DataShard int - ParityShard int - DSCP int - SockBuf int - AckNodelay bool - NoDelay int - Interval int - Resend int - NoCongestion int - SndWnd int - RcvWnd int - MTU int -} -type server struct { - DataShard int - ParityShard int - DSCP int - SockBuf int -} - -func init() { - transports["KCP"] = &kcpTran{ - client: client{ - DataShard: 10, - ParityShard: 3, - DSCP: 0, - SockBuf: 4194304, - NoDelay: 0, - Interval: 50, - Resend: 0, - NoCongestion: 0, - SndWnd: 0, - RcvWnd: 0, - MTU: 1350, - }, - server: server{ - DataShard: 10, - ParityShard: 3, - DSCP: 0, - SockBuf: 4194304, - }, - } -} - -func (c *client) Dial(server string) (net.Conn, error) { - conn, err := kcp.DialWithOptions(server, nil, c.DataShard, c.ParityShard) - if err != nil { - return nil, errors.Wrap(err, "dial") - } - - conn.SetStreamMode(true) - conn.SetWriteDelay(false) - conn.SetNoDelay(c.NoDelay, c.Interval, c.Resend, c.NoCongestion) - conn.SetWindowSize(c.SndWnd, c.RcvWnd) - conn.SetMtu(c.MTU) - conn.SetACKNoDelay(c.AckNodelay) - - if err := conn.SetDSCP(c.DSCP); err != nil { - return nil, errors.Wrap(err, "SetDSCP") - } - if err := conn.SetReadBuffer(c.SockBuf); err != nil { - return nil, errors.Wrap(err, "SetReadBuffer") - } - if err := conn.SetWriteBuffer(c.SockBuf); err != nil { - return nil, errors.Wrap(err, "SetWriteBuffer") - } - - return conn, nil -} - -func (s *server) Listen(port string) (<-chan net.Conn, error) { - ln, err := kcp.ListenWithOptions(port, nil, s.DataShard, s.ParityShard) - if err != nil { - return nil, err - } - - if err := ln.SetDSCP(s.DSCP); err != nil { - return nil, errors.Wrap(err, "SetDSCP") - } - if err := ln.SetReadBuffer(s.SockBuf); err != nil { - return nil, errors.Wrap(err, "SetReadBuffer") - } - if err := ln.SetWriteBuffer(s.SockBuf); err != nil { - return nil, errors.Wrap(err, "SetWriteBuffer") - } - - connCh := make(chan net.Conn) - go func() { - for { - conn, err := ln.AcceptKCP() - if err != nil { - glog.Fatalln("KCP listen:", err) - } - - connCh <- conn - } - }() - - return connCh, nil -} diff --git a/proxy/transport/quic.go b/proxy/transport/quic.go deleted file mode 100644 index 36114fa..0000000 --- a/proxy/transport/quic.go +++ /dev/null @@ -1,137 +0,0 @@ -package transport - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "math/big" - "net" - "time" - - "github.com/golang/glog" - quic "github.com/lucas-clemente/quic-go" - "github.com/pkg/errors" - "github.com/wweir/sower/util" -) - -type quicTran struct { - clientConf *quic.Config - sess quic.Session - - serverConf *quic.Config -} - -func init() { - transports["QUIC"] = &quicTran{ - - clientConf: &quic.Config{ - HandshakeTimeout: time.Second, - KeepAlive: true, - IdleTimeout: time.Minute, - }, - serverConf: &quic.Config{ - MaxIncomingStreams: 1024, - }, - } -} - -func (c *quicTran) Dial(server string) (net.Conn, error) { - if c.sess == nil { - if sess, err := quic.DialAddr(server, &tls.Config{InsecureSkipVerify: true}, c.clientConf); err != nil { - return nil, errors.Wrap(err, "session") - } else { - go func() { - <-sess.Context().Done() - sess.Close() - c.sess = nil - }() - c.sess = sess - } - } - - var stream quic.Stream - if err := util.WithTimeout(func() (err error) { - if stream, err = c.sess.OpenStream(); err != nil { - c.sess = nil - } - return - }, time.Second); err != nil { - return nil, errors.Wrap(err, "stream") - } - - return &streamConn{ - Stream: stream, - sess: c.sess, - }, nil -} - -type streamConn struct { - quic.Stream - sess quic.Session -} - -func (s *streamConn) LocalAddr() net.Addr { - return s.sess.LocalAddr() -} - -func (s *streamConn) RemoteAddr() net.Addr { - return s.sess.RemoteAddr() -} - -func mockTlsPem() *tls.Config { - key, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - glog.Fatalln(err) - } - template := x509.Certificate{SerialNumber: big.NewInt(1)} - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) - if err != nil { - glog.Fatalln(err) - } - - certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) - keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) - - tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - glog.Fatalln(err) - } - return &tls.Config{Certificates: []tls.Certificate{tlsCert}} -} - -func (s *quicTran) Listen(port string) (<-chan net.Conn, error) { - ln, err := quic.ListenAddr(port, mockTlsPem(), s.serverConf) - if err != nil { - return nil, errors.WithStack(err) - } - - connCh := make(chan net.Conn) - go func() { - for { - sess, err := ln.Accept(context.Background()) - if err != nil { - glog.Fatalln(err) - } - go accept(sess, connCh) - } - }() - return connCh, nil -} - -func accept(sess quic.Session, connCh chan<- net.Conn) { - glog.V(1).Infoln("new session from ", sess.RemoteAddr()) - defer sess.Close() - - for { - stream, err := sess.AcceptStream(context.Background()) - if err != nil { - glog.Errorln(err) - return - } - - connCh <- &streamConn{stream, sess} - } -} diff --git a/proxy/transport/tcp.go b/proxy/transport/tcp.go deleted file mode 100644 index b3b876a..0000000 --- a/proxy/transport/tcp.go +++ /dev/null @@ -1,58 +0,0 @@ -package transport - -import ( - "net" - "time" - - "github.com/golang/glog" -) - -type tcp struct { - DialTimeout time.Duration - isSocks5 bool -} - -func init() { - transports["TCP"] = &tcp{ - DialTimeout: 5 * time.Second, - } - transports["SOCKS5"] = &tcp{ - DialTimeout: 5 * time.Second, - isSocks5: true, - } -} - -func (t *tcp) Dial(server string) (net.Conn, error) { - conn, err := net.DialTimeout("tcp", server, t.DialTimeout) - if err != nil { - return nil, err - } - - conn.(*net.TCPConn).SetKeepAlive(true) - return conn, nil -} - -func (t *tcp) Listen(port string) (<-chan net.Conn, error) { - if t.isSocks5 { - panic("not support run as socks5 server") - } - - ln, err := net.Listen("tcp", port) - if err != nil { - return nil, err - } - - connCh := make(chan net.Conn) - go func() { - for { - conn, err := ln.Accept() - if err != nil { - glog.Fatalln("TCP listen:", err) - } - - conn.(*net.TCPConn).SetKeepAlive(true) - connCh <- conn - } - }() - return connCh, nil -} diff --git a/proxy/transport/util.go b/proxy/transport/util.go deleted file mode 100644 index 15e1229..0000000 --- a/proxy/transport/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package transport - -import ( - "net" - - "github.com/pkg/errors" -) - -type Transport interface { - Dial(server string) (net.Conn, error) - Listen(port string) (<-chan net.Conn, error) -} - -var transports = map[string]Transport{} - -func ListTransports() []string { - list := make([]string, 0, len(transports)) - for key := range transports { - list = append(list, key) - } - return list -} - -func GetTransport(netType string) (Transport, error) { - tran, ok := transports[netType] - if !ok { - return nil, errors.New("invalid net type: " + netType) - } - return tran, nil -} diff --git a/proxy/util.go b/proxy/util.go deleted file mode 100644 index 2210bb2..0000000 --- a/proxy/util.go +++ /dev/null @@ -1,53 +0,0 @@ -package proxy - -import ( - "io" - "net" - "sync" - "sync/atomic" - "time" - - "github.com/golang/glog" -) - -// race safe -var resolved = false - -func resolveAddr(server *string) { - if !resolved { - if addr, err := net.ResolveTCPAddr("tcp", *server); err != nil { - glog.Errorln(err) - } else { - glog.Infof("remote server (%s)=>(%s)", *server, addr) - *server = addr.String() - resolved = true - } - } -} - -func relay(conn1, conn2 net.Conn) { - wg := &sync.WaitGroup{} - exitFlag := new(int32) - wg.Add(2) - go redirect(conn2, conn1, wg, exitFlag) - redirect(conn1, conn2, wg, exitFlag) - wg.Wait() -} - -func redirect(dst, src net.Conn, wg *sync.WaitGroup, exitFlag *int32) { - if _, err := io.Copy(dst, src); err != nil { - glog.V(1).Infof("%s<>%s -> %s<>%s: %s", src.RemoteAddr(), src.LocalAddr(), dst.LocalAddr(), dst.RemoteAddr(), err) - } - - if atomic.CompareAndSwapInt32(exitFlag, 0, 1) { - // wakeup blocked goroutine - now := time.Now() - src.SetDeadline(now) - dst.SetDeadline(now) - } else { - src.Close() - dst.Close() - } - - wg.Done() -} diff --git a/util/pick_iface_test.go b/util/pick_iface_test.go deleted file mode 100644 index b81b6f2..0000000 --- a/util/pick_iface_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package util - -import ( - "runtime" - "testing" -) - -func TestPickInterface(t *testing.T) { - switch runtime.GOOS { - case "windows": - case "darwin": - default: - t.Skip("skip for some enviroment not have net interface") - return - } - - got, err := PickInterface() - if err != nil { - t.Errorf("PickInterface() error = %v", err) - } else { - t.Logf("PickInterface() got: MAC: %s IP: %s", got.HardwareAddr, got.IP) - } -} diff --git a/util/util.go b/util/util.go deleted file mode 100644 index 69cea98..0000000 --- a/util/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package util - -import ( - "errors" - "net" - "time" -) - -// Iface is net interface address info -type Iface struct { - net.HardwareAddr - net.IP -} - -func WithTimeout(fn func() error, timeout time.Duration) error { - var okCh = make(chan struct{}) - var err error - - go func() { - err = fn() - close(okCh) - }() - - select { - case <-okCh: - return err - case <-time.After(timeout): - return errors.New("timeout: " + timeout.String()) - } -} From 1a24286bbc5136ee03262d85aa305120b787a182 Mon Sep 17 00:00:00 2001 From: wweir Date: Sat, 15 Feb 2020 10:49:57 +0800 Subject: [PATCH 2/6] Refactor to tls --- .github/Dockerfile | 2 +- .github/cc.wweir.sower.plist | 2 +- .github/install | 127 -------------------- .github/sower-client.service | 2 +- .github/sower-server.service | 2 +- .github/uninstall | 20 ---- Makefile | 2 +- README.md | 2 + conf/conf.go | 160 +++++++++++++------------ conf/conf_darwin.go | 6 +- conf/conf_linux.go | 7 +- conf/conf_windows.go | 9 +- conf/dynamic_rule.go | 132 ++++++++++++++++++++ conf/sower.toml | 33 +++-- dns/dns.go | 111 ++--------------- dns/util.go | 20 ---- go.mod | 5 +- go.sum | 11 +- internal/http/tgt_parser.go | 145 ++++++++++++++++++++++ internal/http/tgt_parser_test.go | 66 ++++++++++ mux/sni.go => internal/http/tgt_sni.go | 2 +- internal/net/dhcp_test.go | 15 +++ internal/net/pick_iface_other_test.go | 15 +++ main.go | 84 ++----------- mux/parse.go | 42 ------- proxy/http_proxy.go | 115 ++++++++++++++++++ proxy/proxy.go | 138 +++++++++++++++++++++ proxy/util.go | 31 +++++ util/suffix_tree.go | 10 +- util/suffix_tree_test.go | 12 +- util/tee_conn.go | 4 + 31 files changed, 825 insertions(+), 507 deletions(-) delete mode 100755 .github/install delete mode 100755 .github/uninstall create mode 100644 conf/dynamic_rule.go create mode 100644 internal/http/tgt_parser.go create mode 100644 internal/http/tgt_parser_test.go rename mux/sni.go => internal/http/tgt_sni.go (99%) create mode 100644 internal/net/dhcp_test.go create mode 100644 internal/net/pick_iface_other_test.go delete mode 100644 mux/parse.go create mode 100644 proxy/http_proxy.go create mode 100644 proxy/proxy.go create mode 100644 proxy/util.go diff --git a/.github/Dockerfile b/.github/Dockerfile index d2bc1f7..810a75a 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -4,7 +4,7 @@ FROM golang:1.13-alpine AS compiler RUN apk add --no-cache git make # enable go modules -WORKDIR /workdir +WORKDIR /src COPY . . # do not worry about downloading dependency, sower will fix this. diff --git a/.github/cc.wweir.sower.plist b/.github/cc.wweir.sower.plist index d5e8b09..912c03f 100644 --- a/.github/cc.wweir.sower.plist +++ b/.github/cc.wweir.sower.plist @@ -8,7 +8,7 @@ /usr/local/bin/sower -f - /usr/local/etc/sower.toml + /etc/sower/sower.toml KeepAlive diff --git a/.github/install b/.github/install deleted file mode 100755 index 7c677d0..0000000 --- a/.github/install +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -set -e -[[ -d /tmp/sower ]] || mkdir /tmp/sower -cd /tmp/sower - -echo ======== Install sower to your system ======== -succ_message(){ - if [[ -z $IP ]]; then - IP=127.0.0.1 - fi - echo - echo ======== Installed sower to your system ======== - echo The config file is: /usr/local/etc/sower.toml - echo please set $IP as the first domain name server manually. -} - -# main logic -VERSION="$(curl -s "https://api.github.com/repos/wweir/sower/releases/latest" | awk -F '"' '/tag_name/{printf $4}')" -case "$(uname -s)" in - "Darwin") - curl -SLf https://github.com/wweir/sower/releases/download/$VERSION/sower-darwin-amd64.tar.gz | tar xzv - sudo mkdir -p /usr/local/bin/ - sudo mv sower /usr/local/bin/ - - if [[ -r /usr/local/etc/sower.toml ]]; then - echo The config file already exists, keep the original file - else - printf "Please enter remote server adddress: " - read ADDRESS - printf "Please enter remote server password (default: 12345678): " - read PASSWORD - if [[ -z $PASSWORD ]]; then - PASSWORD="12345678" - fi - printf "Please enter which IP do you wanna listen (default: 127.0.0.1):" - read IP - if [[ -z $IP ]]; then - IP="127.0.0.1" - fi - echo - - sed -i~ "s/# server_addr=\"remote-server/server_addr=\"$ADDRESS/" sower.toml - sed -i~ "s/client_ip=\"127.0.0.1\"/client_ip=\"$IP\"/" sower.toml - sed -i~ "s/# clear_dns_cache/clear_dns_cache/" sower.toml - sed -i~ "s/\"12345678\"/\"$PASSWORD\"/" sower.toml - sudo mkdir -p /usr/local/etc/ - sudo mv sower.toml /usr/local/etc/ - fi - - echo - echo Register auto start service, root privilege is needed! - echo - sudo mv cc.wweir.sower.plist /Library/LaunchDaemons/ - sudo chown root:wheel /Library/LaunchDaemons/cc.wweir.sower.plist - sudo launchctl load -w /Library/LaunchDaemons/cc.wweir.sower.plist - succ_message - ;; - - "Linux") - if [[ "$(cat /proc/1/comm)" != "systemd" ]]; then - echo do not support auto deploy on SysVinit - exit 1 - fi - printf "Server side or client side, which do you wanna install [c/s]: " - read SIDE - echo - - case "$SIDE" in - "c") - curl -SLf https://github.com/wweir/sower/releases/download/$VERSION/sower-linux-amd64.tar.gz | tar xzv - - sudo mv sower /usr/local/bin/ - - if [[ -r /usr/local/etc/sower.toml ]]; then - echo The config file already exists, keep the original file - else - printf "Please enter remote server adddress: " - read ADDRESS - printf "Please enter remote server password (default: 12345678): " - read PASSWORD - if [[ -z $PASSWORD ]]; then - PASSWORD="12345678" - fi - printf "Please enter which IP do you wanna listen (default: 127.0.0.1):" - read IP - if [[ -z $IP ]]; then - IP="127.0.0.1" - fi - echo - - sed -i~ "s/# server_addr=\"remote-server/server_addr=\"$ADDRESS/" sower.toml - sed -i~ "s/client_ip=\"127.0.0.1\"/client_ip=\"$IP\"/" sower.toml - sed -i~ "s/\"12345678\"/\"$PASSWORD\"/" sower.toml - sudo mv sower.toml /usr/local/etc/ - fi - - sudo mv sower-client.service /etc/systemd/system/ - sudo systemctl enable sower-client - sudo systemctl start sower-client - succ_message - ;; - - "s") - curl -SLf https://github.com/wweir/sower/releases/download/$VERSION/sower-linux-amd64.tar.gz | tar xzv - printf "Please enter remote server password (default: 12345678): " - read PASSWORD - echo - if [[ -z $PASSWORD ]]; then - PASSWORD="12345678" - fi - - sudo mv sower /usr/local/bin/ - sed -i~ "s%bin/sower%bin/sower -p $PASSWORD%" sower-server.service - sudo mv sower-server.service /etc/systemd/system/ - sudo systemctl enable sower-server - sudo systemctl start sower-server - ;; - - *) - echo "invalid: $SIDE" && exit 1 - ;; - esac - ;; - *) - echo Do not support auto deploy on this platform. - ;; -esac diff --git a/.github/sower-client.service b/.github/sower-client.service index 9b63bae..54b519b 100644 --- a/.github/sower-client.service +++ b/.github/sower-client.service @@ -9,6 +9,6 @@ WantedBy=multi-user.target Type=simple User=root WorkingDirectory=/tmp -ExecStart=/usr/local/bin/sower -f /usr/local/etc/sower.toml +ExecStart=/usr/local/bin/sower -f /etc/sower/sower.toml RestartSec=3 Restart=on-failure diff --git a/.github/sower-server.service b/.github/sower-server.service index d016b1c..8d00b5c 100644 --- a/.github/sower-server.service +++ b/.github/sower-server.service @@ -9,6 +9,6 @@ WantedBy=multi-user.target Type=simple User=nobody WorkingDirectory=/tmp -ExecStart=/usr/local/bin/sower -n TCP -v 1 +ExecStart=/usr/local/bin/sower -s 127.0.0.1:8080 RestartSec=3 Restart=on-failure diff --git a/.github/uninstall b/.github/uninstall deleted file mode 100755 index 25404b1..0000000 --- a/.github/uninstall +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -u -x -e - -# main logic -case "$(uname -s)" in - "Darwin") - sudo launchctl unload /Library/LaunchDaemons/cc.wweir.sower.plist - sudo rm -rf /Library/LaunchDaemons/cc.wweir.sower.plist - rm -rf /usr/local/bin/sower /usr/local/etc/sower.toml - ;; - "Linux") - sudo systemctl disable sower-client ||true - sudo systemctl disable sower-server || true - sudo rm -rf /etc/systemd/system/sower-client.service /etc/systemd/system/sower-server.service - sudo rm -rf /usr/local/bin/sower /usr/local/etc/sower.toml - ;; - *) - echo Do not support auto deploy on this platform. - ;; -esac diff --git a/Makefile b/Makefile index d738977..e85072e 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ ifeq ("", "$(shell which stringer)") go get -v golang.org/x/tools/cmd/stringer endif go generate ./... - + cd transport/grpc; protoc --gogofaster_out=plugins=grpc:. *.proto test: go vet ./... go test ./... diff --git a/README.md b/README.md index cbec81e..dc29d36 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ [![GitHub star](https://img.shields.io/github/stars/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/stargazers) [![GitHub license](https://img.shields.io/github/license/wweir/sower.svg?style=popout)](LICENSE) +# **In refactor** + 中文介绍见 [Wiki](https://github.com/wweir/sower/wiki) The sower is a cross-platform intelligent transparent proxy tool base on DNS solution. diff --git a/conf/conf.go b/conf/conf.go index 4442fa8..4f13398 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -4,12 +4,46 @@ import ( "flag" "os" "sync" + "time" toml "github.com/pelletier/go-toml" "github.com/wweir/sower/util" "github.com/wweir/utils/log" ) +type client struct { + Address string `toml:"address"` + + HTTPProxy struct { + Address string `toml:"address"` + } `toml:"http_proxy"` + + DNS struct { + RedirectIP string `toml:"redirect_ip"` + Relay string `toml:"relay"` + FlushCmd string `toml:"flush_cmd"` + } `toml:"dns"` + + Router struct { + PortMapping map[string]string `toml:"port_mapping"` + DetectLevel int `toml:"detect_level"` + DetectTimeout string `toml:"detect_timeout"` + + ProxyList []string `toml:"proxy_list"` + DirectList []string `toml:"direct_list"` + DynamicList []string `toml:"dynamic_list"` + DirectRules *util.Node `toml:"-"` + ProxyRules *util.Node `toml:"-"` + DynamicRules *util.Node `toml:"-"` + } `toml:"router"` +} +type server struct { + Relay string `toml:"relay"` + CertFile string `toml:"cert_file"` + KeyFile string `toml:"key_file"` + CertEmail string `toml:"cert_email"` +} + var ( version, date string @@ -17,54 +51,53 @@ var ( flushMu = sync.Mutex{} flushCh = make(chan struct{}) - // Conf define the config items - Conf = struct { - ConfigFile string - - Upstream struct { - Socks5 string `toml:"socks5"` - DNS string `toml:"dns"` - } `toml:"upstream"` - - Downstream struct { - ServeIP string `toml:"serve_ip"` - HTTPProxy string `toml:"http_proxy"` - } `toml:"downstream"` - - Router struct { - FlushDNSCmd string `toml:"flush_dns_cmd"` - ProxyLevel int `toml:"proxy_level"` - - PortMapping map[string]string `toml:"port_mapping"` - ProxyList []string `toml:"proxy_list"` - DirectList []string `toml:"direct_list"` - DynamicList []string `toml:"dynamic_list"` - } `toml:"router"` - }{} + Server = server{} + Client = client{} + conf = struct { + file string + Server *server `toml:"server"` + Client *client `toml:"client"` + }{"", &Server, &Client} + Password string ) func init() { - flag.StringVar(&Conf.ConfigFile, "f", "", "config file, keep empty for dynamic detect proxy rule") - flag.StringVar(&Conf.Upstream.Socks5, "socks5", "127.0.0.1:1080", "upstream socks5 address") - flag.StringVar(&Conf.Upstream.DNS, "dns", "", "upstream dns ip, keep empty to dynamic detect") - flag.StringVar(&Conf.Downstream.ServeIP, "serve", "127.0.0.1", "serve on address") - flag.StringVar(&Conf.Downstream.HTTPProxy, "http_proxy", "", "serve http proxy, eg: 127.0.0.1:8080") - flag.IntVar(&Conf.Router.ProxyLevel, "level", 2, "dynamic proxy level: 0~4") + var err error + defer func() { + if timeout, err = time.ParseDuration(Client.Router.DetectTimeout); err != nil { + log.Fatalw("parse dynamic detect timeout", "val", Client.Router.DetectTimeout, "err", err) + } + + log.Infow("start", "version", version, "date", date, "conf", &conf) + passwordData = []byte(Password) + }() + + flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set") + flag.StringVar(&Password, "passwd", "", "password, use domain if not set") + flag.StringVar(&Server.Relay, "s", "", "relay to http service, eg: 127.0.0.1:8080") + flag.StringVar(&Server.CertFile, "s_cert", "", "tls cert file, empty to auto get cert") + flag.StringVar(&Server.KeyFile, "s_key", "", "tls key file, empty to auto get cert") + flag.StringVar(&Client.Address, "c", "", "remote server, eg: aa.bb.cc") // TODO: socks5://127.0.0.1:1080 + flag.StringVar(&Client.HTTPProxy.Address, "http_proxy", ":8080", "http proxy, empty to disable") + flag.StringVar(&Client.DNS.RedirectIP, "dns_redirect", "", "redirect ip, eg: 127.0.0.1, empty to disable dns") + flag.StringVar(&Client.DNS.Relay, "dns_relay", "", "dns relay server ip, keep empty to dynamic detect") + flag.IntVar(&Client.Router.DetectLevel, "level", 2, "dynamic rule detect level: 0~4") + flag.StringVar(&Client.Router.DetectTimeout, "timeout", "300ms", "dynamic rule detect timeout") Init() // execute platform init logic if !flag.Parsed() { flag.Parse() } - if _, err := os.Stat(Conf.ConfigFile); err == nil { - for i := range loadConfigFns { - if err := loadConfigFns[i].fn(); err != nil { - log.Fatalw("load config", "config", Conf.ConfigFile, "step", loadConfigFns[i].step, "err", err) - } - } + if conf.file == "" { + return } - log.Infow("start", "version", version, "date", date, "config", Conf) + for i := range loadConfigFns { + if err = loadConfigFns[i].fn(); err != nil { + log.Fatalw("load config", "config", conf.file, "step", loadConfigFns[i].step, "err", err) + } + } } // refreshFns will be executed while init and write new config @@ -72,66 +105,39 @@ var loadConfigFns = []struct { step string fn func() error }{{"load_config", func() error { - f, err := os.OpenFile(Conf.ConfigFile, os.O_RDONLY, 0644) + f, err := os.OpenFile(conf.file, os.O_RDONLY, 0644) if err != nil { return err } defer f.Close() - //safe refresh config - file := Conf.ConfigFile - if err = toml.NewDecoder(f).Decode(&Conf); err != nil { - return err - } - Conf.ConfigFile = file + return toml.NewDecoder(f).Decode(&conf) + +}}, {"load_rules", func() error { + Client.Router.DirectRules = util.NewNodeFromRules(Client.Router.DirectList...) + Client.Router.ProxyRules = util.NewNodeFromRules(Client.Router.ProxyList...) + Client.Router.DynamicRules = util.NewNodeFromRules(Client.Router.DynamicList...) return nil }}, {"flush_dns", func() error { - if Conf.Router.FlushDNSCmd != "" { - return execute(Conf.Router.FlushDNSCmd) + if Client.DNS.FlushCmd != "" { + return execute(Client.DNS.FlushCmd) } return nil }}} -// AddReloadConfigHook add hook function for reload config -func AddReloadConfigHook(step string, fn func() error) { - loadConfigFns = append(loadConfigFns, struct { - step string - fn func() error - }{step, fn}) -} - -// AddDynamic add new domain into dynamic list -func AddDynamic(domain string) { - flushMu.Lock() - Conf.Router.DynamicList = append(Conf.Router.DynamicList, domain) - Conf.Router.DynamicList = util.NewReverseSecSlice(Conf.Router.DynamicList).Sort().Uniq() - flushMu.Unlock() - - flushOnce.Do(func() { - if Conf.ConfigFile != "" { - go flushConf() - } - }) - - select { - case flushCh <- struct{}{}: - default: - } -} - func flushConf() { for range flushCh { // safe write - if Conf.ConfigFile != "" { - f, err := os.OpenFile(Conf.ConfigFile+"~", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if conf.file != "" { + f, err := os.OpenFile(conf.file+"~", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { log.Errorw("flush config", "step", "flush", "err", err) continue } flushMu.Lock() - if err := toml.NewEncoder(f).ArraysWithOneElementPerLine(true).Encode(Conf); err != nil { + if err := toml.NewEncoder(f).ArraysWithOneElementPerLine(true).Encode(conf); err != nil { log.Errorw("flush config", "step", "flush", "err", err) flushMu.Unlock() f.Close() @@ -140,7 +146,7 @@ func flushConf() { flushMu.Unlock() f.Close() - if err = os.Rename(Conf.ConfigFile+"~", Conf.ConfigFile); err != nil { + if err = os.Rename(conf.file+"~", conf.file); err != nil { log.Errorw("flush config", "step", "flush", "err", err) continue } diff --git a/conf/conf_darwin.go b/conf/conf_darwin.go index 7ac14df..abb782c 100644 --- a/conf/conf_darwin.go +++ b/conf/conf_darwin.go @@ -11,16 +11,16 @@ import ( ) func Init() { - flag.StringVar(&Conf.Router.FlushDNSCmd, "flush_dns", "pkill mDNSResponder || true", "flush dns command") + flag.StringVar(&Client.DNS.FlushCmd, "flush_dns", "pkill mDNSResponder || true", "flush dns command") } func execute(cmd string) error { ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() - out, err := exec.CommandContext(ctx, "sh", "-c", Conf.Router.FlushDNSCmd).CombinedOutput() + out, err := exec.CommandContext(ctx, "sh", "-c", Client.DNS.FlushCmd).CombinedOutput() if err != nil { - return fmt.Errorf("cmd: %s, err: %s, output: %s", Conf.Router.FlushDNSCmd, err, out) + return fmt.Errorf("cmd: %s, err: %s, output: %s", Client.DNS.FlushCmd, err, out) } return nil } diff --git a/conf/conf_linux.go b/conf/conf_linux.go index c8e598d..3c52e1f 100644 --- a/conf/conf_linux.go +++ b/conf/conf_linux.go @@ -4,22 +4,23 @@ package conf import ( "context" + "flag" "fmt" "os/exec" "time" ) func Init() { - flag.StringVar(&Conf.Router.FlushDNSCmd, "flush_dns", "", "flush dns command") + flag.StringVar(&Client.DNS.FlushCmd, "flush_dns", "", "flush dns command") } func execute(cmd string) error { ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() - out, err := exec.CommandContext(ctx, "sh", "-c", Conf.Router.FlushDNSCmd).CombinedOutput() + out, err := exec.CommandContext(ctx, "sh", "-c", Client.DNS.FlushCmd).CombinedOutput() if err != nil { - return fmt.Errorf("cmd: %s, err: %s, output: %s", Conf.Router.FlushDNSCmd, err, out) + return fmt.Errorf("cmd: %s, err: %s, output: %s", Client.DNS.FlushCmd, err, out) } return nil } diff --git a/conf/conf_windows.go b/conf/conf_windows.go index 04f25ed..c0fee67 100644 --- a/conf/conf_windows.go +++ b/conf/conf_windows.go @@ -25,12 +25,10 @@ const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndCon func Init() { exePath, _ := filepath.Abs(os.Args[0]) - logFile := filepath.Join(filepath.Dir(exePath), name+".log") install := flag.Bool("i", false, "install sower as a service") uninstall := flag.Bool("u", false, "uninstall sower from service list") - logFile := flag.String("log", logFile, name+" log file path") - flag.StringVar(&Conf.Router.FlushDNSCmd, "flush_dns", "ipconfig /flushdnss", "flush dns command") + flag.StringVar(&Client.DNS.FlushCmd, "flush_dns", "ipconfig /flushdnss", "flush dns command") flag.Parse() switch { @@ -42,6 +40,7 @@ func Init() { return fmt.Errorf("service %s already exists", name) } s, err = m.CreateService(name, exePath, mgr.Config{ + DisplayName: "Sower Proxy", StartType: windows.SERVICE_AUTO_START, }) @@ -153,7 +152,7 @@ func execute(cmd string) error { defer cancel() var cmds []string - for _, cmd := range strings.Split(Conf.ClearDNSCache, " ") { + for _, cmd := range strings.Split(Client.DNS.FlushCmd, " ") { if cmd == "" { continue } @@ -170,7 +169,7 @@ func execute(cmd string) error { command := exec.CommandContext(ctx, cmds[0], cmds[1:]...) command.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} if out, err := command.CombinedOutput(); err != nil { - return fmt.Errorf("cmd: %s, output: %s, err: %w", Conf.ClearDNSCache, out, err) + return fmt.Errorf("cmd: %s, output: %s, err: %w", Client.DNS.FlushCmd, out, err) } return nil } diff --git a/conf/dynamic_rule.go b/conf/dynamic_rule.go new file mode 100644 index 0000000..e6176f5 --- /dev/null +++ b/conf/dynamic_rule.go @@ -0,0 +1,132 @@ +package conf + +import ( + "crypto/tls" + "net" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/util" + "github.com/wweir/utils/log" + "github.com/wweir/utils/mem" +) + +type dynamic struct { + port http.Port +} + +var cache = mem.New(2 * time.Hour) +var detect = &dynamic{} +var passwordData []byte +var timeout time.Duration + +// ShouldProxy check if the domain shoule request though proxy +func ShouldProxy(domain string) bool { + if Client.Router.DirectRules.Match(domain) { + return false + } + if Client.Router.ProxyRules.Match(domain) { + return true + } + if Client.Router.DynamicRules.Match(domain) { + return true + } + + cache.Remember(detect, domain) + return Client.Router.DynamicRules.Match(domain) +} + +func (d *dynamic) Get(key interface{}) (err error) { + // break deadloop, for ugly wildcard setting dns setting + domain := strings.TrimSuffix(key.(string), ".") + if strings.Count(domain, ".") > 10 { + return nil + } + + wg := sync.WaitGroup{} + httpScore, httpsScore := new(int32), new(int32) + for _, ping := range [...]dynamic{{port: http.HTTP}, {port: http.HTTPS}} { + wg.Add(1) + go func(ping dynamic) { + defer wg.Done() + + if err := ping.port.Ping(domain, timeout); err != nil { + return + } + + switch ping.port { + case http.HTTP: + if !atomic.CompareAndSwapInt32(httpScore, 0, 2) { + atomic.AddInt32(httpScore, 1) + } + case http.HTTPS: + if !atomic.CompareAndSwapInt32(httpsScore, 0, 2) { + atomic.AddInt32(httpScore, 1) + } + } + }(ping) + } + for _, ping := range [...]dynamic{{port: http.HTTP}, {port: http.HTTPS}} { + wg.Add(1) + go func(ping dynamic) { + defer wg.Done() + + var conn net.Conn + if conn, err = tls.Dial("tcp", net.JoinHostPort(Client.Address, "443"), &tls.Config{}); err != nil { + log.Errorw("tls dial", "addr", net.JoinHostPort(Client.Address, "443"), "err", err) + return + } + + if ping.port == http.HTTP { + conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTP, "", 80) + } else { + conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTPS, "", 443) + } + + if err := ping.port.PingWithConn(domain, conn, timeout); err != nil { + return + } + + switch ping.port { + case http.HTTP: + if !atomic.CompareAndSwapInt32(httpScore, 0, -2) { + atomic.AddInt32(httpScore, -1) + } + case http.HTTPS: + if !atomic.CompareAndSwapInt32(httpsScore, 0, -2) { + atomic.AddInt32(httpScore, -1) + } + } + }(ping) + } + + wg.Wait() + if int(*httpScore+*httpsScore)+conf.Client.Router.DetectLevel < 0 { + addDynamic(domain) + log.Infow("add rule", "domain", domain, "http_score", *httpScore, "https_score", *httpsScore) + } + return nil +} + +// addDynamic add new domain into dynamic list +func addDynamic(domain string) { + flushMu.Lock() + Client.Router.DynamicList = util.NewReverseSecSlice( + append(Client.Router.DynamicList, domain)).Sort().Uniq() + Client.Router.DynamicRules = util.NewNodeFromRules(Client.Router.DynamicList...) + flushMu.Unlock() + + flushOnce.Do(func() { + if conf.file != "" { + go flushConf() + } + }) + + select { + case flushCh <- struct{}{}: + default: + } +} diff --git a/conf/sower.toml b/conf/sower.toml index c07cd12..03a9788 100644 --- a/conf/sower.toml +++ b/conf/sower.toml @@ -1,17 +1,28 @@ -[upstream] -socks5="127.0.0.1:1080" -dns="" # eg: 223.5.5.5, keep empty to get it from network environment +# [server] +# relay="127.0.0.1:8080" +# cert_file="" +# key_file="" +# cert_email="abc@email.com" -[downstream] -serve_ip="127.0.0.1" -http_proxy="" # eg: :8080, 127.0.0.1:8080, keep empty to disable it -[router.port_mapping] -# 2222="aaa.bbb.cc:22" +[client] +address="aa.bb.cc" + +[client.http_proxy] +address=":8080" + +[client.dns] +redirect_ip="127.0.0.1" +relay="" # empty to dynamic detect +flush_cmd="" # macOS: pkill mDNSResponder || true, Windows: ipconfig /flushdnss + +# [client.router.port_mapping] +# ":2222"="aa.bb.cc:22" + +[client.router] +detect_level=2 # 0~4, the bigger the harder to add +detect_timeout="300ms" -[router] -flush_dns_cmd="" # Platform-related, keep empty to use default command -proxy_level=2 # dynamic detect level, (0~4), more bigger more harder to be proxy proxy_list=[ "**.google.*", # google "**.goo.gl", diff --git a/dns/dns.go b/dns/dns.go index 59ea225..1968d5b 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -1,38 +1,27 @@ package dns -/* - * Deep integration with package conf: github.com/wweir/sower/conf - */ import ( - "context" "fmt" "net" "strings" - "sync" - "sync/atomic" "time" "github.com/miekg/dns" "github.com/wweir/sower/conf" - "github.com/wweir/sower/internal/http" - internal_net "github.com/wweir/sower/internal/net" - "github.com/wweir/sower/internal/socks5" + _net "github.com/wweir/sower/internal/net" "github.com/wweir/utils/log" - "github.com/wweir/utils/mem" ) -func ServeDNS() { - serveIP := net.ParseIP(conf.Conf.Downstream.ServeIP) - if conf.Conf.Downstream.ServeIP == "" || serveIP.String() != conf.Conf.Downstream.ServeIP { - log.Fatalw("invalid listen ip", "ip", conf.Conf.Downstream.ServeIP) +func ServeDNS(redirectIP, relayServer string) { + serveIP := net.ParseIP(redirectIP) + if redirectIP == "" || serveIP.String() != redirectIP { + log.Fatalw("invalid listen ip", "ip", redirectIP) } - dnsServer, err := PickUpstreamDNS(serveIP.String(), conf.Conf.Upstream.DNS) + dnsServer, err := PickUpstreamDNS(serveIP.String(), relayServer) if err != nil { log.Fatalw("") } - d := &detect{proxy: conf.Conf.Upstream.Socks5} - dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { // *Msg r has an TSIG record and it was validated if r.IsTsig() != nil && w.TsigStatus() == nil { @@ -50,7 +39,7 @@ func ServeDNS() { domain = domain[:idx] // trim port } - if err := matchAndServe(w, r, serveIP, d, domain, dnsServer); err != nil { + if err := matchAndServe(w, r, serveIP, domain, dnsServer); err != nil { server, err := PickUpstreamDNS(serveIP.String(), dnsServer) if err != nil { log.Errorw("detect upstream dns fail", "err", err) @@ -66,7 +55,7 @@ func ServeDNS() { func PickUpstreamDNS(listenIP string, dnsServer string) (string, error) { if dnsServer == "" { - return internal_net.GetDefaultDNSServer() + return _net.GetDefaultDNSServer() } if _, port, err := net.SplitHostPort(dnsServer); err != nil { @@ -77,22 +66,13 @@ func PickUpstreamDNS(listenIP string, dnsServer string) (string, error) { return dnsServer, nil } -const timeout = 200 * time.Millisecond - -func matchAndServe(w dns.ResponseWriter, r *dns.Msg, serveIP net.IP, d *detect, domain, dnsServer string) error { - if (!whiteList.Match(domain)) && - (blockList.Match(domain) || suggestList.Match(domain)) { +func matchAndServe(w dns.ResponseWriter, r *dns.Msg, serveIP net.IP, domain, dnsServer string) error { + if conf.ShouldProxy(domain) { w.WriteMsg(localA(r, domain, serveIP)) return nil } - if err := mem.Remember(d, domain); err != nil { - panic(err) - } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - msg, err := dns.ExchangeContext(ctx, r, dnsServer) + msg, err := dns.Exchange(r, dnsServer) if err != nil || msg == nil { return err } @@ -100,72 +80,3 @@ func matchAndServe(w dns.ResponseWriter, r *dns.Msg, serveIP net.IP, d *detect, w.WriteMsg(msg) return nil } - -type detect struct { - proxy string - port http.Port -} - -func (d *detect) Get(key interface{}) error { - // break deadloop, for ugly wildcard setting dns setting - domain := strings.TrimSuffix(key.(string), ".") - if strings.Count(domain, ".") > 10 { - return nil - } - - wg := sync.WaitGroup{} - httpScore, httpsScore := new(int32), new(int32) - for _, ping := range [...]detect{{"", http.HTTP}, {"", http.HTTPS}} { - wg.Add(1) - go func(ping detect) { - defer wg.Done() - - if err := ping.port.Ping(domain, timeout); err != nil { - return - } - - switch ping.port { - case http.HTTP: - if !atomic.CompareAndSwapInt32(httpScore, 0, 2) { - atomic.AddInt32(httpScore, 1) - } - case http.HTTPS: - if !atomic.CompareAndSwapInt32(httpsScore, 0, 2) { - atomic.AddInt32(httpScore, 1) - } - } - }(ping) - } - for _, ping := range [...]detect{{d.proxy, http.HTTP}, {d.proxy, http.HTTPS}} { - wg.Add(1) - go func(ping detect) { - defer wg.Done() - - if conn, err := net.Dial("tcp", ping.proxy); err != nil { - return - } else { - conn = socks5.ToSocks5(conn, domain, ping.port.String()) - if err := ping.port.PingWithConn(domain, conn, timeout); err != nil { - return - } - } - - switch ping.port { - case http.HTTP: - if !atomic.CompareAndSwapInt32(httpScore, 0, -2) { - atomic.AddInt32(httpScore, -1) - } - case http.HTTPS: - if !atomic.CompareAndSwapInt32(httpsScore, 0, -2) { - atomic.AddInt32(httpScore, -1) - } - } - }(ping) - } - - wg.Wait() - if int(*httpScore+*httpsScore) >= conf.Conf.Router.ProxyLevel { - conf.AddDynamic(domain) - } - return nil -} diff --git a/dns/util.go b/dns/util.go index 42d50cd..4c4ebf0 100644 --- a/dns/util.go +++ b/dns/util.go @@ -4,28 +4,8 @@ import ( "net" "github.com/miekg/dns" - "github.com/wweir/sower/conf" - "github.com/wweir/sower/util" - "github.com/wweir/utils/log" ) -var blockList *util.Node -var suggestList *util.Node -var whiteList *util.Node - -func init() { - reloadFn := func() error { - whiteList = util.NewNodeFromRules(".", conf.Conf.Router.DirectList...) - blockList = util.NewNodeFromRules(".", conf.Conf.Router.ProxyList...) - suggestList = util.NewNodeFromRules(".", conf.Conf.Router.DynamicList...) - log.Infow("reload config rules") - return nil - } - - reloadFn() - conf.AddReloadConfigHook("reload rules", reloadFn) -} - func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg { m := new(dns.Msg) m.SetReply(r) diff --git a/go.mod b/go.mod index f0308dd..7948dba 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/miekg/dns v1.1.27 github.com/pelletier/go-toml v1.6.0 github.com/pkg/errors v0.9.1 - github.com/wweir/utils v0.0.0-20200201061343-d44781e298da - golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 + github.com/wweir/utils v0.0.0-20200214114658-f6f356a08736 + golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 + golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe ) diff --git a/go.sum b/go.sum index caf112c..bfbc677 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f h1:QatZ4lsJBY3x1+Imst9g95+vUl7m52dqM9Pi4aSMW8w= github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f/go.mod h1:BNLmYJ8oMJPIPpNx5968jCyUhwEU1XT3YsuOqtbo5qo= -github.com/wweir/utils v0.0.0-20200201061343-d44781e298da h1:d4/Y71orUtfo1Z8AnKKCQWBqupjY9zZpCsoU3G5hssw= -github.com/wweir/utils v0.0.0-20200201061343-d44781e298da/go.mod h1:Nv4eBGkUJiHDPgVowJJlQNcJYgMfst6IkjaDThH2/yI= +github.com/wweir/utils v0.0.0-20200214114658-f6f356a08736 h1:x6LiUrnHR4CCqzRzB0zqiB9hzVFcRhSKrVlZDGW0FcM= +github.com/wweir/utils v0.0.0-20200214114658-f6f356a08736/go.mod h1:Nv4eBGkUJiHDPgVowJJlQNcJYgMfst6IkjaDThH2/yI= go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= @@ -55,8 +55,9 @@ go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -72,10 +73,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/internal/http/tgt_parser.go b/internal/http/tgt_parser.go new file mode 100644 index 0000000..c2f3e46 --- /dev/null +++ b/internal/http/tgt_parser.go @@ -0,0 +1,145 @@ +package http + +import ( + "bufio" + "crypto/md5" + "encoding/binary" + "errors" + "io" + "net" + "net/http" + "strconv" + "strings" + + "github.com/wweir/sower/util" +) + +const ( + TGT_OTHER byte = iota + TGT_HTTP + TGT_HTTPS +) + +// Write Addr +type conn struct { + typ byte + password []byte + domain []byte + port uint16 + init bool + net.Conn +} + +func NewTgtConn(c net.Conn, password []byte, tgtType byte, domain string, port uint16) net.Conn { + return &conn{ + typ: tgtType, + password: password, + domain: []byte(domain), + port: port, + init: true, + Conn: c, + } +} + +// other => type + checksum + port + domain_length ++ domain + data +// http => type + checksum ++ data +// https => type + checksum + port ++ data +type header struct { + Type byte + Checksum byte + Port uint16 + DomainLength uint8 +} + +func (c *conn) Write(b []byte) (n int, err error) { + if c.init { + c.init = false + domainLength := byte(len(c.domain)) + if err := binary.Write(c.Conn, binary.BigEndian, &header{ + Type: c.typ, + Checksum: checksum(c.password, c.port, domainLength), + Port: c.port, + DomainLength: domainLength, + }); err != nil { + return 0, err + } + + n, err := c.Conn.Write(append(c.domain, b...)) + return n - len(c.domain), err + } + + return c.Conn.Write(b) +} + +// ParseAddr parse target addr from net.Conn +func ParseAddr(conn net.Conn, password []byte) (c net.Conn, domain string, port uint16, err error) { + teeConn := &util.TeeConn{Conn: conn} + teeConn.StartOrReset() + defer func() { + if err != nil { + teeConn.Close() + } else { + teeConn.Stop() + } + }() + + head := new(header) + if err = binary.Read(conn, binary.BigEndian, head); err != nil { + return teeConn, "", 0, nil + } + if head.Checksum != checksum(password, head.Port, head.DomainLength) { + return teeConn, "", 0, nil + } + + switch head.Type { + case TGT_OTHER: + buf := make([]byte, int(head.DomainLength)) + if _, err = io.ReadFull(conn, buf); err != nil { + return teeConn, "", 0, err + } + + return teeConn, string(buf), head.Port, nil + + case TGT_HTTP: + teeConn.DropAndRestart() + + var resp *http.Request + resp, err = http.ReadRequest(bufio.NewReader(teeConn)) + if err != nil { + return nil, "", 0, err + } + + idx := strings.LastIndex(resp.Host, ":") + if idx == -1 { + return teeConn, resp.Host, 80, nil + } + + var port uint64 + if port, err = strconv.ParseUint(resp.Host[idx+1:], 10, 16); err != nil { + return nil, "", 0, err + } + return teeConn, resp.Host[:idx], uint16(port), nil + + case TGT_HTTPS: + teeConn.DropAndRestart() + + domain, _, err := extractSNI(teeConn) + if err != nil { + return nil, "", 0, err + } + return teeConn, domain, head.Port, nil + + default: + return nil, "", 0, errors.New("invalid request") + } +} + +var errChecksum = errors.New("invalid checksum") + +func checksum(password []byte, port uint16, length uint8) (val byte) { + nums := md5.Sum(append(password, byte(port), length)) + for _, b := range nums { + val += b + } + return val +} diff --git a/internal/http/tgt_parser_test.go b/internal/http/tgt_parser_test.go new file mode 100644 index 0000000..8e97c18 --- /dev/null +++ b/internal/http/tgt_parser_test.go @@ -0,0 +1,66 @@ +package http + +import ( + "bufio" + "bytes" + "io/ioutil" + "net" + "net/http" + "testing" +) + +func TestParseAddr1(t *testing.T) { + c1, c2 := net.Pipe() + + go func() { + c1 = NewTgtConn(c1, nil, TGT_HTTP, "", 0) + req, _ := http.NewRequest("GET", "http://wweir.cc", bytes.NewReader([]byte{1, 2, 3})) + req.Write(c1) + }() + + c2, host, port, err := ParseAddr(c2, nil) + + if err != nil || host != "wweir.cc" || port != 80 { + t.Error(err, host, port) + } + + req, err := http.ReadRequest(bufio.NewReader(c2)) + if err != nil { + t.Error(err) + } + + data, err := ioutil.ReadAll(req.Body) + if err != nil || len(data) != 3 || data[0] != 1 { + t.Error(err, data) + } +} + +func TestParseAddr2(t *testing.T) { + c1, c2 := net.Pipe() + + go func() { + c1 = NewTgtConn(c1, nil, TGT_HTTPS, "", 443) + c1.Write(HTTPS.PingMsg("wweir.cc")) + }() + + _, host, port, err := ParseAddr(c2, nil) + + if err != nil || host != "wweir.cc" || port != 443 { + t.Error(err, host, port) + } +} + +func TestParseAddr3(t *testing.T) { + c1, c2 := net.Pipe() + + go func() { + c1 = NewTgtConn(c1, nil, TGT_OTHER, "wweir.cc", 1080) + c1.Write(HTTPS.PingMsg("wweir.cc")) + }() + + _, host, port, err := ParseAddr(c2, nil) + + if err != nil || host != "wweir.cc" || port != 1080 { + t.Error(err, host, port) + } +} diff --git a/mux/sni.go b/internal/http/tgt_sni.go similarity index 99% rename from mux/sni.go rename to internal/http/tgt_sni.go index 687da76..55b10c5 100644 --- a/mux/sni.go +++ b/internal/http/tgt_sni.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mux +package http import ( "encoding/binary" diff --git a/internal/net/dhcp_test.go b/internal/net/dhcp_test.go new file mode 100644 index 0000000..25fbcc2 --- /dev/null +++ b/internal/net/dhcp_test.go @@ -0,0 +1,15 @@ +package net_test + +import ( + "fmt" + + "github.com/wweir/sower/internal/net" +) + +func Example_dns() { + got, err := net.GetDefaultDNSServer() + if err != nil { + panic(err) + } + fmt.Println(got) +} diff --git a/internal/net/pick_iface_other_test.go b/internal/net/pick_iface_other_test.go new file mode 100644 index 0000000..9d6c96c --- /dev/null +++ b/internal/net/pick_iface_other_test.go @@ -0,0 +1,15 @@ +package net_test + +import ( + "fmt" + + "github.com/wweir/sower/internal/net" +) + +func Example_iface() { + got, err := net.PickInternetInterface() + if err != nil { + panic(err) + } + fmt.Println(got) +} diff --git a/main.go b/main.go index d4785a5..03cc463 100644 --- a/main.go +++ b/main.go @@ -1,89 +1,23 @@ package main import ( - "io" - "net" - "sync" - "sync/atomic" - "time" - "github.com/wweir/sower/conf" "github.com/wweir/sower/dns" - "github.com/wweir/sower/internal/socks5" - "github.com/wweir/sower/mux" - "github.com/wweir/utils/log" + "github.com/wweir/sower/proxy" ) func main() { - go dns.ServeDNS() - - go proxy(conf.Conf.Upstream.Socks5, conf.Conf.Downstream.ServeIP, "80", mux.ParseHTTP) - go proxy(conf.Conf.Upstream.Socks5, conf.Conf.Downstream.ServeIP, "443", mux.ParseHTTPS) - - for port, target := range conf.Conf.Router.PortMapping { - go proxy(conf.Conf.Upstream.Socks5, conf.Conf.Downstream.ServeIP, port, - func(conn net.Conn) (net.Conn, string, error) { - return conn, target, nil - }) + if conf.Server.Relay != "" { + proxy.StartServer(conf.Server.Relay, conf.Password, + conf.Server.CertFile, conf.Server.KeyFile, conf.Server.CertEmail) } - select {} -} - -func proxy(socks5Addr, serveIP, port string, mux func(net.Conn) (net.Conn, string, error)) { - ln, err := net.Listen("tcp", net.JoinHostPort(serveIP, port)) - if err != nil { - log.Fatalw("listen", "ip", serveIP, "port", port, "err", err) - } - - log.Infow("start proxy", "port", port) - for { - conn, err := ln.Accept() - if err != nil { - log.Fatalw("listen", "ip", serveIP, "port", port, "err", err) + if conf.Client.Address != "" { + if conf.Client.DNS.RedirectIP != "" { + go dns.ServeDNS(conf.Client.DNS.RedirectIP, conf.Server.Relay) } - if conn, target, err := mux(conn); err != nil { - log.Errorw("mux parse", "target", target, "err", err) - continue - } else { - go relay(socks5Addr, target, conn) - } + proxy.StartClient(conf.Password, conf.Client.Address, conf.Client.HTTPProxy.Address, + conf.Client.DNS.RedirectIP, conf.Client.Router.PortMapping) } } - -func relay(socks5Addr, target string, conn net.Conn) { - var socks5Conn net.Conn - if c, err := net.Dial("tcp", socks5Addr); err != nil { - log.Errorw("dial socks5", "addr", socks5Addr, "err", err) - return - } else if host, port, err := net.SplitHostPort(target); err != nil { - log.Errorw("dial socks5", "target", target, "err", err) - return - } else { - socks5Conn = socks5.ToSocks5(c, host, port) - } - - wg := &sync.WaitGroup{} - exitFlag := new(int32) - wg.Add(2) - go redirect(conn, socks5Conn, wg, exitFlag) - redirect(socks5Conn, conn, wg, exitFlag) - wg.Wait() -} - -func redirect(dst, src net.Conn, wg *sync.WaitGroup, exitFlag *int32) { - io.Copy(dst, src) - - if atomic.CompareAndSwapInt32(exitFlag, 0, 1) { - // wakeup blocked goroutine - now := time.Now() - src.SetDeadline(now) - dst.SetDeadline(now) - } else { - src.Close() - dst.Close() - } - - wg.Done() -} diff --git a/mux/parse.go b/mux/parse.go deleted file mode 100644 index 99dcc0f..0000000 --- a/mux/parse.go +++ /dev/null @@ -1,42 +0,0 @@ -package mux - -import ( - "bufio" - "io" - "net" - "net/http" - "strings" - - "github.com/wweir/sower/util" -) - -func ParseHTTP(conn net.Conn) (net.Conn, string, error) { - teeConn := &util.TeeConn{Conn: conn} - teeConn.StartOrReset() - defer teeConn.Stop() - - b := bufio.NewReader(teeConn) - resp, err := http.ReadRequest(b) - if err != nil { - return teeConn, "", err - } - if strings.Contains(resp.Host, ":") { - return teeConn, resp.Host, nil - } - return teeConn, resp.Host + ":80", nil -} - -func ParseHTTPS(conn net.Conn) (net.Conn, string, error) { - teeConn := &util.TeeConn{Conn: conn} - teeConn.StartOrReset() - defer teeConn.Stop() - - host, _, err := extractSNI(io.Reader(teeConn)) - if err != nil { - return teeConn, "", err - } - if strings.Contains(host, ":") { - return teeConn, host, nil - } - return teeConn, host + ":443", nil -} diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go new file mode 100644 index 0000000..d759f30 --- /dev/null +++ b/proxy/http_proxy.go @@ -0,0 +1,115 @@ +package proxy + +import ( + "context" + "crypto/tls" + "io" + "net" + "net/http" + "strconv" + "time" + + "github.com/wweir/sower/conf" + _http "github.com/wweir/sower/internal/http" + "github.com/wweir/utils/log" +) + +func startHTTPProxy(httpProxyAddr, serverAddr string, password []byte) { + srv := &http.Server{ + Addr: httpProxyAddr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + httpsProxy(w, r, serverAddr, password) + } else { + httpProxy(w, r, serverAddr, password) + } + }), + // Disable HTTP/2. + TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, + IdleTimeout: 90 * time.Second, + } + + go log.Fatalw("serve http proxy", "addr", httpProxyAddr, "err", srv.ListenAndServe()) +} + +func httpProxy(w http.ResponseWriter, r *http.Request, serverAddr string, password []byte) { + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + } + + roundTripper := &http.Transport{} + if conf.ShouldProxy(host) { + roundTripper.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) + if err != nil { + return nil, err + } + + return _http.NewTgtConn(conn, password, _http.TGT_HTTP, "", 80), nil + } + } + + resp, err := roundTripper.RoundTrip(r) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + defer resp.Body.Close() + + for k, vs := range resp.Header { + for _, v := range vs { + w.Header().Add(k, v) + } + } + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + +func httpsProxy(w http.ResponseWriter, r *http.Request, serverAddr string, password []byte) { + var host string + var port = uint16(443) + if h, p, err := net.SplitHostPort(r.Host); err != nil { + host = r.Host + } else if pNum, err := strconv.ParseUint(p, 10, 16); err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } else { + host = h + port = uint16(pNum) + } + + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + conn.(*net.TCPConn).SetKeepAlive(true) + + if _, err := conn.Write([]byte(r.Proto + " 200 Connection established\r\n\r\n")); err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + conn.Close() + return + } + + var rc net.Conn + if conf.ShouldProxy(host) { + rc, err = tls.Dial("tcp", serverAddr, &tls.Config{}) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + conn.Close() + return + } + rc = _http.NewTgtConn(rc, password, _http.TGT_HTTPS, "", port) + + } else { + rc, err = net.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(int(port)))) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + conn.Close() + return + } + } + + relay(conn, rc) +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..cca8fdf --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,138 @@ +package proxy + +import ( + "crypto/tls" + "net" + "net/http" + "strconv" + + _http "github.com/wweir/sower/internal/http" + "github.com/wweir/utils/log" + "golang.org/x/crypto/acme/autocert" +) + +const configDir = "/etc/sower" + +type head struct { + checksum byte + length byte +} + +func StartClient(password, serverAddr, httpProxy, dnsRedirectIP string, forwardMap map[string]string) { + passwordData := []byte(password) + if httpProxy != "" { + startHTTPProxy(httpProxy, serverAddr, passwordData) + } + + relayToRemote := func(tgtType byte, lnAddr string, host string, port uint16) { + ln, err := net.Listen("tcp", lnAddr) + if err != nil { + log.Fatalw("tcp listen", "port", lnAddr, "err", err) + } + + for { + conn, err := ln.Accept() + if err != nil { + log.Errorw("tcp accept", "port", lnAddr, "err", err) + continue + } + + go func(conn net.Conn) { + rc, err := tls.Dial("tcp", serverAddr, &tls.Config{}) + if err != nil { + log.Errorw("tls dial", "addr", serverAddr, "err", err) + return + } + + relay(conn, _http.NewTgtConn(rc, passwordData, tgtType, host, port)) + }(conn) + } + } + + if dnsRedirectIP != "" { + go relayToRemote(_http.TGT_HTTP, dnsRedirectIP+":http", "", 80) + go relayToRemote(_http.TGT_HTTPS, dnsRedirectIP+":http", "", 443) + } + + for from, to := range forwardMap { + go func(from, to string) { + host, portStr, err := net.SplitHostPort(to) + if err != nil { + log.Fatalw("parse port forward", "target", to, "err", err) + } + portNum, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + log.Fatalw("parse port forward", "target", to, "err", err) + } + port := uint16(portNum) + + relayToRemote(_http.TGT_OTHER, from, host, port) + }(from, to) + } + +} + +func StartServer(relayTarget, password, certFile, keyFile, email string) { + certManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(configDir), //folder for storing certificates + Email: email, + } + tlsConf := &tls.Config{GetCertificate: certManager.GetCertificate} + if certFile != "" && keyFile != "" { + if cert, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil { + log.Fatalw("load certificate", "cert", certFile, "key", keyFile, "err", err) + } else { + tlsConf = &tls.Config{Certificates: []tls.Certificate{cert}} + } + } + + // Try to redirect 80 to 443 + go http.ListenAndServe(":http", certManager.HTTPHandler(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if host, _, err := net.SplitHostPort(r.Host); err != nil { + r.URL.Host = r.Host + } else { + r.URL.Host = host + } + r.URL.Scheme = "https" + http.Redirect(w, r, r.URL.String(), 301) + }))) + + ln, err := tls.Listen("tcp", ":https", tlsConf) + if err != nil { + log.Fatalw("tcp listen", "err", err) + } + + passwordData := []byte(password) + for { + conn, err := ln.Accept() + if err != nil { + log.Errorw("tcp accept", "err", err) + continue + } + + go func(conn net.Conn) { + conn, domain, port, err := _http.ParseAddr(conn, passwordData) + if err != nil { + log.Errorw("parse relay target", "err", err) + return + } + defer conn.Close() + + addr := relayTarget + if domain != "" { + addr = net.JoinHostPort(domain, strconv.Itoa(int(port))) + } + + rc, err := net.Dial("tcp", addr) + if err != nil { + log.Errorw("tcp dial", "host", domain, "addr", addr, "err", err) + return + } + defer rc.Close() + + relay(conn, rc) + }(conn) + } +} diff --git a/proxy/util.go b/proxy/util.go new file mode 100644 index 0000000..efffeaa --- /dev/null +++ b/proxy/util.go @@ -0,0 +1,31 @@ +package proxy + +import ( + "io" + "net" + "sync" + "sync/atomic" + "time" +) + +func relay(conn1, conn2 net.Conn) { + wg := &sync.WaitGroup{} + exitFlag := new(int32) + wg.Add(2) + go redirect(conn2, conn1, wg, exitFlag) + redirect(conn1, conn2, wg, exitFlag) + wg.Wait() +} + +func redirect(dst, src net.Conn, wg *sync.WaitGroup, exitFlag *int32) { + io.Copy(dst, src) + + if atomic.CompareAndSwapInt32(exitFlag, 0, 1) { + // wakeup blocked goroutine + now := time.Now() + src.SetDeadline(now) + dst.SetDeadline(now) + } + + wg.Done() +} diff --git a/util/suffix_tree.go b/util/suffix_tree.go index ff59765..ad13835 100644 --- a/util/suffix_tree.go +++ b/util/suffix_tree.go @@ -14,11 +14,8 @@ type node struct { node map[string]*node } -func NewNode(sep string) *Node { - return &Node{node{node: map[string]*node{}}, sep, &sync.RWMutex{}} -} -func NewNodeFromRules(sep string, rules ...string) *Node { - n := NewNode(sep) +func NewNodeFromRules(rules ...string) *Node { + n := &Node{node{node: map[string]*node{}}, ".", &sync.RWMutex{}} for i := range rules { n.Add(rules[i]) } @@ -69,6 +66,9 @@ func (n *node) add(secs []string) { } func (n *Node) Match(item string) bool { + if n == nil { + return false + } return n.matchSecs(strings.Split(n.trim(item), n.sep), false) } diff --git a/util/suffix_tree_test.go b/util/suffix_tree_test.go index cca33e3..b338b1c 100644 --- a/util/suffix_tree_test.go +++ b/util/suffix_tree_test.go @@ -15,14 +15,14 @@ func TestNode_Match(t *testing.T) { tests []test }{{ "simple", - NewNodeFromRules(".", "a.wweir.cc", "b.wweir.cc"), + NewNodeFromRules("a.wweir.cc", "b.wweir.cc"), []test{ {"a.wweir.cc", true}, {"b.wweir.cc", true}, }, }, { "parent", - NewNodeFromRules(".", "wweir.cc", "a.wweir.cc"), + NewNodeFromRules("wweir.cc", "a.wweir.cc"), []test{ {"wweir.cc", true}, {"a.wweir.cc", true}, @@ -30,7 +30,7 @@ func TestNode_Match(t *testing.T) { }, }, { "fuzz1", - NewNodeFromRules(".", "wweir.cc", "a.wweir.cc", "*.wweir.cc"), + NewNodeFromRules("wweir.cc", "a.wweir.cc", "*.wweir.cc"), []test{ {"wweir.cc", true}, {"a.wweir.cc", true}, @@ -39,7 +39,7 @@ func TestNode_Match(t *testing.T) { }, }, { "fuzz2", - NewNodeFromRules(".", "a.*.cc", "c.wweir.*"), + NewNodeFromRules("a.*.cc", "c.wweir.*"), []test{ {"wweir.cc", false}, {"a.wweir.cc", true}, @@ -48,7 +48,7 @@ func TestNode_Match(t *testing.T) { }, }, { "fuzz3", - NewNodeFromRules(".", "*.*.cc", "iamp.*.*"), + NewNodeFromRules("*.*.cc", "iamp.*.*"), []test{ {"wweir.cc", false}, {"a.wweir.cc", true}, @@ -57,7 +57,7 @@ func TestNode_Match(t *testing.T) { }, }, { "fuzz4", - NewNodeFromRules(".", "**.cc", "a.**.com", "**.wweir.*"), + NewNodeFromRules("**.cc", "a.**.com", "**.wweir.*"), []test{ {"wweir.cc", true}, {"a.wweir.cc", true}, diff --git a/util/tee_conn.go b/util/tee_conn.go index 641c8c7..89a2485 100644 --- a/util/tee_conn.go +++ b/util/tee_conn.go @@ -15,6 +15,10 @@ func (t *TeeConn) StartOrReset() { t.offset = 0 t.tee = true } +func (t *TeeConn) DropAndRestart() { + t.buf = []byte{} + t.tee = true +} func (t *TeeConn) Stop() { t.offset = 0 t.tee = false From 85226890c5ab283999289fb24aa103327a8ac05b Mon Sep 17 00:00:00 2001 From: wweir Date: Sat, 15 Feb 2020 16:47:08 +0800 Subject: [PATCH 3/6] Support run as system service --- .github/Dockerfile | 3 +- .github/cc.wweir.sower.plist | 18 -------- .github/sower-client.service | 14 ------- .github/sower-server.service | 14 ------- Makefile | 39 ++---------------- README.md | 23 ++++++++++- conf/conf.go | 36 ++++++++++------ conf/conf_darwin.go | 53 ++++++++++++++++++++++-- conf/conf_linux.go | 57 +++++++++++++++++++++++--- conf/conf_windows.go | 79 ++++++++++++++++++------------------ 10 files changed, 191 insertions(+), 145 deletions(-) delete mode 100644 .github/cc.wweir.sower.plist delete mode 100644 .github/sower-client.service delete mode 100644 .github/sower-server.service diff --git a/.github/Dockerfile b/.github/Dockerfile index 810a75a..aa0e4f2 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -7,12 +7,11 @@ RUN apk add --no-cache git make WORKDIR /src COPY . . -# do not worry about downloading dependency, sower will fix this. RUN CGO_ENABLED=0 make build # Build image FROM scratch -COPY --from=compiler /workdir/sower /sower +COPY --from=compiler /src/sower /sower ENTRYPOINT [ "/sower" ] \ No newline at end of file diff --git a/.github/cc.wweir.sower.plist b/.github/cc.wweir.sower.plist deleted file mode 100644 index 912c03f..0000000 --- a/.github/cc.wweir.sower.plist +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Label - cc.wweir.sower - ProgramArguments - - /usr/local/bin/sower - -f - /etc/sower/sower.toml - - KeepAlive - - RunAtLoad - - - diff --git a/.github/sower-client.service b/.github/sower-client.service deleted file mode 100644 index 54b519b..0000000 --- a/.github/sower-client.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Sower client service -After=network.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=root -WorkingDirectory=/tmp -ExecStart=/usr/local/bin/sower -f /etc/sower/sower.toml -RestartSec=3 -Restart=on-failure diff --git a/.github/sower-server.service b/.github/sower-server.service deleted file mode 100644 index 8d00b5c..0000000 --- a/.github/sower-server.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Sower server service -After=network.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=nobody -WorkingDirectory=/tmp -ExecStart=/usr/local/bin/sower -s 127.0.0.1:8080 -RestartSec=3 -Restart=on-failure diff --git a/Makefile b/Makefile index e85072e..c2ba3d3 100644 --- a/Makefile +++ b/Makefile @@ -1,43 +1,12 @@ -export GO111MODULE=on -SERVER:=127.0.0.1:5533 - default: test build -generate: -ifeq ("", "$(shell which stringer)") - @echo installing generator - go get -v golang.org/x/tools/cmd/stringer -endif - go generate ./... - cd transport/grpc; protoc --gogofaster_out=plugins=grpc:. *.proto test: go vet ./... - go test ./... + go list ./... | grep -v internal | xargs go test build: - go build -ldflags \ - "-X conf.version=$(shell git describe --tags) \ - -X conf.date=$(shell date +%Y-%m-%d)" + go build -ldflags "-w -s \ + -X conf.version=$(shell git describe --tags) \ + -X conf.date=$(shell date +%Y-%m-%d)" image: docker build -t sower -f .github/Dockerfile . -kill: - sudo pkill -9 sower || true - -client: build kill - sudo $(PWD)/sower -s 127.0.0.1:5533 -H "127.0.0.1:8080" - -server: build - $(PWD)/sower -f '' - -run: build kill - $(PWD)/sower -f '' & - sudo $(PWD)/sower -f '' -s 127.0.0.1:5533 -H "127.0.0.1:8080" & - - @sleep 1 - HTTP_PROXY=http://127.0.0.1:8080 curl http://baidu.com || true - @echo - HTTPS_PROXY=http://127.0.0.1:8080 curl https://baidu.com || true - @echo - - @sleep 1 - @sudo pkill -9 sower || true diff --git a/README.md b/README.md index dc29d36..cbba022 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,28 @@ [![GitHub issue](https://img.shields.io/github/issues/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/issues) [![GitHub star](https://img.shields.io/github/stars/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/stargazers) [![GitHub license](https://img.shields.io/github/license/wweir/sower.svg?style=popout)](LICENSE) - +``` + relay <--+ +-> target +http service | | service + +-------+-------+----+ + | sower server | + +----^-----^---------+ + 80 443 +301 http -+ +----- https +to https | service + | + protected + by tls + dns <---+ | +--> direct + relay | | | request + +----+-----+----+----+ + | sower client | + +----^--^----^---^---+ + | | | | + dns --+ + + +-- port + 80 http(s) forward + 443 proxy +``` # **In refactor** 中文介绍见 [Wiki](https://github.com/wweir/sower/wiki) diff --git a/conf/conf.go b/conf/conf.go index 4f13398..5264587 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -58,21 +58,12 @@ var ( Server *server `toml:"server"` Client *client `toml:"client"` }{"", &Server, &Client} - Password string + Password string + installCmd string + uninstallFlag bool ) func init() { - var err error - defer func() { - if timeout, err = time.ParseDuration(Client.Router.DetectTimeout); err != nil { - log.Fatalw("parse dynamic detect timeout", "val", Client.Router.DetectTimeout, "err", err) - } - - log.Infow("start", "version", version, "date", date, "conf", &conf) - passwordData = []byte(Password) - }() - - flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set") flag.StringVar(&Password, "passwd", "", "password, use domain if not set") flag.StringVar(&Server.Relay, "s", "", "relay to http service, eg: 127.0.0.1:8080") flag.StringVar(&Server.CertFile, "s_cert", "", "tls cert file, empty to auto get cert") @@ -83,11 +74,30 @@ func init() { flag.StringVar(&Client.DNS.Relay, "dns_relay", "", "dns relay server ip, keep empty to dynamic detect") flag.IntVar(&Client.Router.DetectLevel, "level", 2, "dynamic rule detect level: 0~4") flag.StringVar(&Client.Router.DetectTimeout, "timeout", "300ms", "dynamic rule detect timeout") - + flag.BoolVar(&uninstallFlag, "uninstall", false, "uninstall service") Init() // execute platform init logic + if !flag.Parsed() { flag.Parse() } + if uninstallFlag { + uninstall() + os.Exit(0) + } + if installCmd != "" { + install() + os.Exit(0) + } + + var err error + defer func() { + if timeout, err = time.ParseDuration(Client.Router.DetectTimeout); err != nil { + log.Fatalw("parse dynamic detect timeout", "val", Client.Router.DetectTimeout, "err", err) + } + + log.Infow("start", "version", version, "date", date, "conf", &conf) + passwordData = []byte(Password) + }() if conf.file == "" { return diff --git a/conf/conf_darwin.go b/conf/conf_darwin.go index abb782c..ef77d99 100644 --- a/conf/conf_darwin.go +++ b/conf/conf_darwin.go @@ -6,21 +6,68 @@ import ( "context" "flag" "fmt" + "io/ioutil" + "os" "os/exec" + "path/filepath" "time" + + "github.com/wweir/utils/log" ) +const svcPath = "/Library/LaunchDaemons/cc.wweir.sower.plist" +const svcFile = ` + + + + Label + cc.wweir.sower + ProgramArguments + + /bin/sh + -c + %s %s + + KeepAlive + + RunAtLoad + + +` + func Init() { + flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set") flag.StringVar(&Client.DNS.FlushCmd, "flush_dns", "pkill mDNSResponder || true", "flush dns command") + flag.StringVar(&installCmd, "install", "", "install service with cmd, eg: '-f /etc/sower/sower.toml'") } +func install() { + execFile, err := filepath.Abs(os.Args[0]) + if err != nil { + log.Fatalw("get binary path", "err", err) + } + + if err := ioutil.WriteFile(svcPath, []byte(fmt.Sprintf(svcFile, execFile, installCmd)), 0644); err != nil { + log.Fatalw("write service file", "err", err) + } + + execute("launchctl unload " + svcPath) + if err := execute("launchctl load -w " + svcPath); err != nil { + log.Fatalw("install service", "err", err) + } +} +func uninstall() { + execute("launchctl unload " + svcPath) + os.Remove(svcPath) + os.RemoveAll("/etc/sower") +} func execute(cmd string) error { - ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) defer cancel() - out, err := exec.CommandContext(ctx, "sh", "-c", Client.DNS.FlushCmd).CombinedOutput() + out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput() if err != nil { - return fmt.Errorf("cmd: %s, err: %s, output: %s", Client.DNS.FlushCmd, err, out) + return fmt.Errorf("cmd: %s, err: %s, output: %s", cmd, err, out) } return nil } diff --git a/conf/conf_linux.go b/conf/conf_linux.go index 3c52e1f..ba5b1a1 100644 --- a/conf/conf_linux.go +++ b/conf/conf_linux.go @@ -6,21 +6,68 @@ import ( "context" "flag" "fmt" + "io/ioutil" + "os" "os/exec" + "path/filepath" "time" + + "github.com/wweir/utils/log" ) -func Init() { - flag.StringVar(&Client.DNS.FlushCmd, "flush_dns", "", "flush dns command") -} +const svcPath = "/etc/systemd/system/sower.service" +const svcFile = `[Unit] +Description=Sower client service +After=network.target +[Install] +WantedBy=multi-user.target + +[Service] +Type=simple +User=root +WorkingDirectory=/tmp +ExecStart=%s %s +RestartSec=3 +Restart=on-failure` + +func Init() { + flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set") + flag.StringVar(&Client.DNS.FlushCmd, "flush_dns", "", "flush dns command") + flag.StringVar(&installCmd, "install", "", "install service with cmd, eg: '-f /etc/sower/sower.toml'") +} +func install() { + execFile, err := filepath.Abs(os.Args[0]) + if err != nil { + log.Fatalw("get binary path", "err", err) + } + + if err := ioutil.WriteFile(svcPath, []byte(fmt.Sprintf(svcFile, execFile, installCmd)), 0644); err != nil { + log.Fatalw("write service file", "err", err) + } + if err := execute("systemctl daemon-reload"); err != nil { + log.Fatalw("install service", "err", err) + } + if err := execute("systemctl enable sower"); err != nil { + log.Fatalw("install service", "err", err) + } + if err := execute("systemctl start sower"); err != nil { + log.Fatalw("install service", "err", err) + } +} +func uninstall() { + execute("systemctl stop sower") + execute("systemctl disable sower") + os.Remove(svcPath) + os.RemoveAll("/etc/sower") +} func execute(cmd string) error { ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() - out, err := exec.CommandContext(ctx, "sh", "-c", Client.DNS.FlushCmd).CombinedOutput() + out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput() if err != nil { - return fmt.Errorf("cmd: %s, err: %s, output: %s", Client.DNS.FlushCmd, err, out) + return fmt.Errorf("cmd: %s, err: %s, output: %s", cmd, err, out) } return nil } diff --git a/conf/conf_windows.go b/conf/conf_windows.go index c0fee67..72539f6 100644 --- a/conf/conf_windows.go +++ b/conf/conf_windows.go @@ -23,51 +23,18 @@ import ( const name = "sower" const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue -func Init() { - exePath, _ := filepath.Abs(os.Args[0]) +var execFile, _ = filepath.Abs(os.Args[0]) +var execDir, _ = filepath.Abs(filepath.Dir(execFile)) - install := flag.Bool("i", false, "install sower as a service") - uninstall := flag.Bool("u", false, "uninstall sower from service list") +func Init() { + flag.StringVar(&conf.file, "f", filepath.Join(execDir, "sower.toml"), "config file, rewrite all other parameters if set") + flag.StringVar(&installCmd, "install", "", "install service with cmd") flag.StringVar(&Client.DNS.FlushCmd, "flush_dns", "ipconfig /flushdnss", "flush dns command") flag.Parse() switch { - case *install: - mgrDo(func(m *mgr.Mgr) error { - s, err := m.OpenService(name) - if err == nil { - s.Close() - return fmt.Errorf("service %s already exists", name) - } - s, err = m.CreateService(name, exePath, mgr.Config{ - - DisplayName: "Sower Proxy", - StartType: windows.SERVICE_AUTO_START, - }) - if err != nil { - return err - } - defer s.Close() - err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info) - if err != nil { - s.Delete() - return fmt.Errorf("SetupEventLogSource() failed: %s", err) - } - - return s.Start() - }) - os.Exit(0) - - case *uninstall: - serviceDo(func(s *mgr.Service) error { - err := s.Delete() - if err != nil { - return err - } - return eventlog.Remove(name) - }) - os.Exit(0) - + case installCmd != "": + case uninstallFlag: default: os.Chdir(filepath.Dir(os.Args[0])) if active, err := svc.IsAnInteractiveSession(); err != nil { @@ -90,7 +57,39 @@ func Init() { } } } +func install() { + mgrDo(func(m *mgr.Mgr) error { + s, err := m.OpenService(name) + if err == nil { + s.Close() + return fmt.Errorf("service %s already exists", name) + } + s, err = m.CreateService(name, execFile, mgr.Config{ + DisplayName: "Sower Proxy", + StartType: windows.SERVICE_AUTO_START, + }) + if err != nil { + return err + } + defer s.Close() + err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info) + if err != nil { + s.Delete() + return fmt.Errorf("SetupEventLogSource() failed: %s", err) + } + return s.Start() + }) +} +func uninstall() { + serviceDo(func(s *mgr.Service) error { + err := s.Delete() + if err != nil { + return err + } + return eventlog.Remove(name) + }) +} func serviceDo(fn func(*mgr.Service) error) { mgrDo(func(m *mgr.Mgr) error { s, err := m.OpenService(name) From 4681dd9845af22d13c0d62a8bc8c4db5d20bbae8 Mon Sep 17 00:00:00 2001 From: wweir Date: Sun, 16 Feb 2020 08:23:16 +0800 Subject: [PATCH 4/6] Debug --- conf/conf.go | 40 +++---- conf/conf_darwin.go | 2 +- conf/dynamic_rule.go | 17 +-- conf/sower.toml | 110 ++++++++---------- dns/dns.go | 82 ------------- dns/util.go | 24 ---- ...iface_other_test.go => pick_iface_test.go} | 0 main.go | 19 ++- proxy/dns.go | 93 +++++++++++++++ proxy/http_proxy.go | 7 +- proxy/proxy.go | 28 +++-- 11 files changed, 210 insertions(+), 212 deletions(-) delete mode 100644 dns/dns.go delete mode 100644 dns/util.go rename internal/net/{pick_iface_other_test.go => pick_iface_test.go} (100%) create mode 100644 proxy/dns.go diff --git a/conf/conf.go b/conf/conf.go index 5264587..b6adf2f 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -19,9 +19,9 @@ type client struct { } `toml:"http_proxy"` DNS struct { - RedirectIP string `toml:"redirect_ip"` - Relay string `toml:"relay"` - FlushCmd string `toml:"flush_cmd"` + ServeIP string `toml:"serve_ip"` + Upstream string `toml:"upstream"` + FlushCmd string `toml:"flush_cmd"` } `toml:"dns"` Router struct { @@ -29,16 +29,16 @@ type client struct { DetectLevel int `toml:"detect_level"` DetectTimeout string `toml:"detect_timeout"` - ProxyList []string `toml:"proxy_list"` - DirectList []string `toml:"direct_list"` - DynamicList []string `toml:"dynamic_list"` - DirectRules *util.Node `toml:"-"` - ProxyRules *util.Node `toml:"-"` - DynamicRules *util.Node `toml:"-"` + ProxyList []string `toml:"proxy_list"` + DirectList []string `toml:"direct_list"` + DynamicList []string `toml:"dynamic_list"` + directRules *util.Node + proxyRules *util.Node + dynamicRules *util.Node } `toml:"router"` } type server struct { - Relay string `toml:"relay"` + Upstream string `toml:"upstream"` CertFile string `toml:"cert_file"` KeyFile string `toml:"key_file"` CertEmail string `toml:"cert_email"` @@ -64,14 +64,14 @@ var ( ) func init() { - flag.StringVar(&Password, "passwd", "", "password, use domain if not set") - flag.StringVar(&Server.Relay, "s", "", "relay to http service, eg: 127.0.0.1:8080") - flag.StringVar(&Server.CertFile, "s_cert", "", "tls cert file, empty to auto get cert") - flag.StringVar(&Server.KeyFile, "s_key", "", "tls key file, empty to auto get cert") - flag.StringVar(&Client.Address, "c", "", "remote server, eg: aa.bb.cc") // TODO: socks5://127.0.0.1:1080 + flag.StringVar(&Password, "password", "", "password") + flag.StringVar(&Server.Upstream, "s", "", "upstream http service, eg: 127.0.0.1:8080") + flag.StringVar(&Server.CertFile, "s_cert", "", "tls cert file, gen cert from letsencrypt if empty") + flag.StringVar(&Server.KeyFile, "s_key", "", "tls key file, gen cert from letsencrypt if empty") + flag.StringVar(&Client.Address, "c", "", "remote server domain, eg: aa.bb.cc") // TODO: socks5://127.0.0.1:1080 flag.StringVar(&Client.HTTPProxy.Address, "http_proxy", ":8080", "http proxy, empty to disable") - flag.StringVar(&Client.DNS.RedirectIP, "dns_redirect", "", "redirect ip, eg: 127.0.0.1, empty to disable dns") - flag.StringVar(&Client.DNS.Relay, "dns_relay", "", "dns relay server ip, keep empty to dynamic detect") + flag.StringVar(&Client.DNS.ServeIP, "dns_ip", "", "upstream dns, eg: 127.0.0.1, disable dns proxy if empty") + flag.StringVar(&Client.DNS.Upstream, "dns_upstream", "", "dns relay server ip, dynamic detect if empty") flag.IntVar(&Client.Router.DetectLevel, "level", 2, "dynamic rule detect level: 0~4") flag.StringVar(&Client.Router.DetectTimeout, "timeout", "300ms", "dynamic rule detect timeout") flag.BoolVar(&uninstallFlag, "uninstall", false, "uninstall service") @@ -124,9 +124,9 @@ var loadConfigFns = []struct { return toml.NewDecoder(f).Decode(&conf) }}, {"load_rules", func() error { - Client.Router.DirectRules = util.NewNodeFromRules(Client.Router.DirectList...) - Client.Router.ProxyRules = util.NewNodeFromRules(Client.Router.ProxyList...) - Client.Router.DynamicRules = util.NewNodeFromRules(Client.Router.DynamicList...) + Client.Router.directRules = util.NewNodeFromRules(Client.Router.DirectList...) + Client.Router.proxyRules = util.NewNodeFromRules(Client.Router.ProxyList...) + Client.Router.dynamicRules = util.NewNodeFromRules(Client.Router.DynamicList...) return nil }}, {"flush_dns", func() error { diff --git a/conf/conf_darwin.go b/conf/conf_darwin.go index ef77d99..96513d3 100644 --- a/conf/conf_darwin.go +++ b/conf/conf_darwin.go @@ -62,7 +62,7 @@ func uninstall() { os.RemoveAll("/etc/sower") } func execute(cmd string) error { - ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput() diff --git a/conf/dynamic_rule.go b/conf/dynamic_rule.go index e6176f5..b38abbc 100644 --- a/conf/dynamic_rule.go +++ b/conf/dynamic_rule.go @@ -25,18 +25,21 @@ var timeout time.Duration // ShouldProxy check if the domain shoule request though proxy func ShouldProxy(domain string) bool { - if Client.Router.DirectRules.Match(domain) { - return false - } - if Client.Router.ProxyRules.Match(domain) { + if domain == Client.Address { return true } - if Client.Router.DynamicRules.Match(domain) { + if Client.Router.directRules.Match(domain) { + return false + } + if Client.Router.proxyRules.Match(domain) { + return true + } + if Client.Router.dynamicRules.Match(domain) { return true } cache.Remember(detect, domain) - return Client.Router.DynamicRules.Match(domain) + return Client.Router.dynamicRules.Match(domain) } func (d *dynamic) Get(key interface{}) (err error) { @@ -116,7 +119,7 @@ func addDynamic(domain string) { flushMu.Lock() Client.Router.DynamicList = util.NewReverseSecSlice( append(Client.Router.DynamicList, domain)).Sort().Uniq() - Client.Router.DynamicRules = util.NewNodeFromRules(Client.Router.DynamicList...) + Client.Router.dynamicRules = util.NewNodeFromRules(Client.Router.DynamicList...) flushMu.Unlock() flushOnce.Do(func() { diff --git a/conf/sower.toml b/conf/sower.toml index 03a9788..8c6d0a6 100644 --- a/conf/sower.toml +++ b/conf/sower.toml @@ -1,65 +1,57 @@ -# [server] -# relay="127.0.0.1:8080" -# cert_file="" -# key_file="" -# cert_email="abc@email.com" - - [client] -address="aa.bb.cc" + address = "aa.bb.cc" -[client.http_proxy] -address=":8080" + [client.dns] + flush_cmd="" # macOS: pkill mDNSResponder || true, Windows: ipconfig /flushdnss + serve_ip = "127.0.0.1" + upstream = "" # empty to dynamic detect -[client.dns] -redirect_ip="127.0.0.1" -relay="" # empty to dynamic detect -flush_cmd="" # macOS: pkill mDNSResponder || true, Windows: ipconfig /flushdnss + [client.http_proxy] + address = ":8080" # empty to disable http_proxy -# [client.router.port_mapping] -# ":2222"="aa.bb.cc:22" + [client.router] + detect_level = 2 # 0~4, the bigger the harder to add + detect_timeout = "300ms" + direct_list = [ + "**.in-addr.arpa", + "imap.*.*", + "imap.*.*.*", + "smtp.*.*", + "smtp.*.*.*", + "pop.*.*", + "pop.*.*.*", + "**.cn", + ] + dynamic_list = [] + proxy_list = [ + "**.google.*", + "**.goo.gl", + "**.googleusercontent.com", + "**.googleapis.com", + "*.googlesource.com", + "**.youtube.com", + "**.ytimg.com", + "**.ggpht.com", + "**.googlevideo.com", + "**.facebook.com", + "**.fbcdn.net", + "**.twitter.com", + "**.twimg.com", + "**.blogspot.com", + "**.appspot.com", + "**.wikipedia.org", + "*.cloudfront.net", + "**.amazon.com", + "**.amazonaws.com", + "*.githubusercontent.com", + "*.github.*", + ] -[client.router] -detect_level=2 # 0~4, the bigger the harder to add -detect_timeout="300ms" + [client.router.port_mapping] + # ":2222"="aa.bb.cc:22" -proxy_list=[ - "**.google.*", # google - "**.goo.gl", - "**.googleusercontent.com", - "**.googleapis.com", - "*.googlesource.com", - "**.youtube.com", # youtube - "**.ytimg.com", - "**.ggpht.com", - "**.googlevideo.com", - "**.facebook.com", # facebook - "**.fbcdn.net", - "**.twitter.com", # twitter - "**.twimg.com", - "**.blogspot.com", # blogspot - "**.appspot.com", - "**.wikipedia.org", # wikipeida - "*.cloudfront.net", - "**.amazon.com", - "**.amazonaws.com", - "*.githubusercontent.com", - "*.github.*", -] -direct_list=[ - "**.in-addr.arpa", - "imap.*.*", - "imap.*.*.*", - "smtp.*.*", - "smtp.*.*.*", - "pop.*.*", - "pop.*.*.*", - "imap-mail.outlook.com", - "**.qq.com", - "**.baidu.com", - "*.aliyun.com", - "**.cn", - "**.icloud.com", - "**.163.com", - "**.weiyun.com", -] +[server] + cert_email = "" # eg: user@aa.bb.cc + cert_file = "" # eg: /etc/ssl/server.crt + key_file = "" # eg: /etc/ssl/server.key + upstream = "" # eg: 127.0.0.1:8080 \ No newline at end of file diff --git a/dns/dns.go b/dns/dns.go deleted file mode 100644 index 1968d5b..0000000 --- a/dns/dns.go +++ /dev/null @@ -1,82 +0,0 @@ -package dns - -import ( - "fmt" - "net" - "strings" - "time" - - "github.com/miekg/dns" - "github.com/wweir/sower/conf" - _net "github.com/wweir/sower/internal/net" - "github.com/wweir/utils/log" -) - -func ServeDNS(redirectIP, relayServer string) { - serveIP := net.ParseIP(redirectIP) - if redirectIP == "" || serveIP.String() != redirectIP { - log.Fatalw("invalid listen ip", "ip", redirectIP) - } - dnsServer, err := PickUpstreamDNS(serveIP.String(), relayServer) - if err != nil { - log.Fatalw("") - } - - dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { - // *Msg r has an TSIG record and it was validated - if r.IsTsig() != nil && w.TsigStatus() == nil { - lastTsig := r.Extra[len(r.Extra)-1].(*dns.TSIG) - r.SetTsig(lastTsig.Hdr.Name, dns.HmacMD5, 300, time.Now().Unix()) - } - - //https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071 - if len(r.Question) == 0 { - return - } - - domain := r.Question[0].Name - if idx := strings.IndexByte(domain, ':'); idx > 0 { - domain = domain[:idx] // trim port - } - - if err := matchAndServe(w, r, serveIP, domain, dnsServer); err != nil { - server, err := PickUpstreamDNS(serveIP.String(), dnsServer) - if err != nil { - log.Errorw("detect upstream dns fail", "err", err) - } else { - dnsServer = server - } - } - }) - - server := &dns.Server{Addr: ":53", Net: "udp"} - log.Fatalw("dns serve fail", "err", server.ListenAndServe()) -} - -func PickUpstreamDNS(listenIP string, dnsServer string) (string, error) { - if dnsServer == "" { - return _net.GetDefaultDNSServer() - } - - if _, port, err := net.SplitHostPort(dnsServer); err != nil { - return "", fmt.Errorf("parse upstream dns(%s) server fail: %w", dnsServer, err) - } else if port == "" { - return net.JoinHostPort(dnsServer, "53"), nil - } - return dnsServer, nil -} - -func matchAndServe(w dns.ResponseWriter, r *dns.Msg, serveIP net.IP, domain, dnsServer string) error { - if conf.ShouldProxy(domain) { - w.WriteMsg(localA(r, domain, serveIP)) - return nil - } - - msg, err := dns.Exchange(r, dnsServer) - if err != nil || msg == nil { - return err - } - - w.WriteMsg(msg) - return nil -} diff --git a/dns/util.go b/dns/util.go deleted file mode 100644 index 4c4ebf0..0000000 --- a/dns/util.go +++ /dev/null @@ -1,24 +0,0 @@ -package dns - -import ( - "net" - - "github.com/miekg/dns" -) - -func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg { - m := new(dns.Msg) - m.SetReply(r) - if localIP.To4() != nil { - m.Answer = []dns.RR{&dns.A{ - Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20}, - A: localIP, - }} - } else { - m.Answer = []dns.RR{&dns.AAAA{ - Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20}, - AAAA: localIP, - }} - } - return m -} diff --git a/internal/net/pick_iface_other_test.go b/internal/net/pick_iface_test.go similarity index 100% rename from internal/net/pick_iface_other_test.go rename to internal/net/pick_iface_test.go diff --git a/main.go b/main.go index 03cc463..f0ba5b3 100644 --- a/main.go +++ b/main.go @@ -1,23 +1,30 @@ package main import ( + "flag" + "fmt" + "github.com/wweir/sower/conf" - "github.com/wweir/sower/dns" "github.com/wweir/sower/proxy" ) func main() { - if conf.Server.Relay != "" { - proxy.StartServer(conf.Server.Relay, conf.Password, + if conf.Server.Upstream != "" { + proxy.StartServer(conf.Server.Upstream, conf.Password, conf.Server.CertFile, conf.Server.KeyFile, conf.Server.CertEmail) } if conf.Client.Address != "" { - if conf.Client.DNS.RedirectIP != "" { - go dns.ServeDNS(conf.Client.DNS.RedirectIP, conf.Server.Relay) + if conf.Client.DNS.ServeIP != "" { + go proxy.ServeDNS(conf.Client.DNS.ServeIP, conf.Client.DNS.Upstream) } proxy.StartClient(conf.Password, conf.Client.Address, conf.Client.HTTPProxy.Address, - conf.Client.DNS.RedirectIP, conf.Client.Router.PortMapping) + conf.Client.DNS.ServeIP, conf.Client.Router.PortMapping) + } + + if conf.Server.Upstream == "" && conf.Client.Address == "" { + fmt.Println() + flag.Usage() } } diff --git a/proxy/dns.go b/proxy/dns.go new file mode 100644 index 0000000..4a65b0d --- /dev/null +++ b/proxy/dns.go @@ -0,0 +1,93 @@ +package proxy + +import ( + "net" + "strings" + "time" + + "github.com/miekg/dns" + "github.com/wweir/sower/conf" + _net "github.com/wweir/sower/internal/net" + "github.com/wweir/utils/log" +) + +func ServeDNS(redirectIP, relayServer string) { + serveIP := net.ParseIP(redirectIP) + if redirectIP == "" || serveIP.String() != redirectIP { + log.Fatalw("invalid listen ip", "ip", redirectIP) + } + + var err error + if relayServer, err = pickRelayAddr(relayServer); err != nil { + log.Fatalw("pick upstream dns server", "err", err) + } + log.Infow("detect upstream dns", "addr", relayServer) + + dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { + // *Msg r has an TSIG record and it was validated + if r.IsTsig() != nil && w.TsigStatus() == nil { + lastTsig := r.Extra[len(r.Extra)-1].(*dns.TSIG) + r.SetTsig(lastTsig.Hdr.Name, dns.HmacMD5, 300, time.Now().Unix()) + } + + //https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071 + if len(r.Question) == 0 { + return + } + + domain := r.Question[0].Name + if idx := strings.IndexByte(domain, ':'); idx > 0 { + domain = domain[:idx] // trim port + } + + if conf.ShouldProxy(domain) { + w.WriteMsg(localA(r, domain, serveIP)) + + } else if msg, err := dns.Exchange(r, relayServer); err != nil || msg == nil { + server, err := pickRelayAddr(relayServer) + if err != nil { + log.Errorw("detect upstream dns", "err", err) + } else { + relayServer = server + log.Infow("detect upstream dns", "addr", relayServer) + } + + } else { + w.WriteMsg(msg) + } + }) + + server := &dns.Server{Addr: net.JoinHostPort(redirectIP, "53"), Net: "udp"} + log.Infow("start dns", "addr", server.Addr) + log.Fatalw("dns serve fail", "err", server.ListenAndServe()) +} + +func pickRelayAddr(relayServer string) (_ string, err error) { + if relayServer == "" { + if relayServer, err = _net.GetDefaultDNSServer(); err != nil { + return "", err + } + } + + if _, _, err := net.SplitHostPort(relayServer); err != nil { + return net.JoinHostPort(relayServer, "53"), nil + } + return relayServer, nil +} + +func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg { + m := new(dns.Msg) + m.SetReply(r) + if localIP.To4() != nil { + m.Answer = []dns.RR{&dns.A{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20}, + A: localIP, + }} + } else { + m.Answer = []dns.RR{&dns.AAAA{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20}, + AAAA: localIP, + }} + } + return m +} diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index d759f30..670372f 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -94,9 +94,9 @@ func httpsProxy(w http.ResponseWriter, r *http.Request, serverAddr string, passw var rc net.Conn if conf.ShouldProxy(host) { - rc, err = tls.Dial("tcp", serverAddr, &tls.Config{}) + rc, err = tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) if err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) + conn.Write([]byte("sower: tls dial: " + err.Error())) conn.Close() return } @@ -105,11 +105,12 @@ func httpsProxy(w http.ResponseWriter, r *http.Request, serverAddr string, passw } else { rc, err = net.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(int(port)))) if err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) + conn.Write([]byte("sower: tcp dial: " + err.Error())) conn.Close() return } } + defer rc.Close() relay(conn, rc) } diff --git a/proxy/proxy.go b/proxy/proxy.go index cca8fdf..45c627d 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -18,10 +18,10 @@ type head struct { length byte } -func StartClient(password, serverAddr, httpProxy, dnsRedirectIP string, forwardMap map[string]string) { +func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards map[string]string) { passwordData := []byte(password) if httpProxy != "" { - startHTTPProxy(httpProxy, serverAddr, passwordData) + go startHTTPProxy(httpProxy, serverAddr, passwordData) } relayToRemote := func(tgtType byte, lnAddr string, host string, port uint16) { @@ -38,23 +38,26 @@ func StartClient(password, serverAddr, httpProxy, dnsRedirectIP string, forwardM } go func(conn net.Conn) { - rc, err := tls.Dial("tcp", serverAddr, &tls.Config{}) + defer conn.Close() + + rc, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) if err != nil { - log.Errorw("tls dial", "addr", serverAddr, "err", err) + log.Errorw("tls dial", "addr", net.JoinHostPort(serverAddr, "443"), "err", err) return } + defer rc.Close() relay(conn, _http.NewTgtConn(rc, passwordData, tgtType, host, port)) }(conn) } } - if dnsRedirectIP != "" { - go relayToRemote(_http.TGT_HTTP, dnsRedirectIP+":http", "", 80) - go relayToRemote(_http.TGT_HTTPS, dnsRedirectIP+":http", "", 443) + if dnsServeIP != "" { + go relayToRemote(_http.TGT_HTTP, dnsServeIP+":http", "", 80) + go relayToRemote(_http.TGT_HTTPS, dnsServeIP+":https", "", 443) } - for from, to := range forwardMap { + for from, to := range forwards { go func(from, to string) { host, portStr, err := net.SplitHostPort(to) if err != nil { @@ -70,6 +73,7 @@ func StartClient(password, serverAddr, httpProxy, dnsRedirectIP string, forwardM }(from, to) } + select {} } func StartServer(relayTarget, password, certFile, keyFile, email string) { @@ -78,12 +82,16 @@ func StartServer(relayTarget, password, certFile, keyFile, email string) { Cache: autocert.DirCache(configDir), //folder for storing certificates Email: email, } - tlsConf := &tls.Config{GetCertificate: certManager.GetCertificate} + tlsConf := &tls.Config{ + GetCertificate: certManager.GetCertificate, + MinVersion: tls.VersionTLS12, + } if certFile != "" && keyFile != "" { if cert, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil { log.Fatalw("load certificate", "cert", certFile, "key", keyFile, "err", err) } else { - tlsConf = &tls.Config{Certificates: []tls.Certificate{cert}} + tlsConf.GetCertificate = nil + tlsConf.Certificates = []tls.Certificate{cert} } } From 7d77f4a85e8a24d730b5bc55a4adea1675cdee43 Mon Sep 17 00:00:00 2001 From: wweir Date: Sun, 16 Feb 2020 16:33:24 +0800 Subject: [PATCH 5/6] Add Socks5 support --- conf/conf.go | 2 +- conf/conf_darwin.go | 4 +-- conf/dynamic_rule.go | 21 ++++++++---- conf/sower.toml | 2 +- go.mod | 2 +- go.sum | 3 +- internal/http/tgt_parser.go | 61 ++++++++++++++++------------------ internal/socks5/rfc_def.go | 44 +++++++++++++++++++++++++ internal/socks5/socks5.go | 66 +++++++++++-------------------------- proxy/dns.go | 2 +- proxy/http_proxy.go | 44 ++++++------------------- proxy/proxy.go | 36 ++++++++++++-------- proxy/util.go | 20 +++++++++++ util/util.go | 19 +++++++++++ 14 files changed, 187 insertions(+), 139 deletions(-) create mode 100644 internal/socks5/rfc_def.go create mode 100644 util/util.go diff --git a/conf/conf.go b/conf/conf.go index b6adf2f..fb5221b 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -68,7 +68,7 @@ func init() { flag.StringVar(&Server.Upstream, "s", "", "upstream http service, eg: 127.0.0.1:8080") flag.StringVar(&Server.CertFile, "s_cert", "", "tls cert file, gen cert from letsencrypt if empty") flag.StringVar(&Server.KeyFile, "s_key", "", "tls key file, gen cert from letsencrypt if empty") - flag.StringVar(&Client.Address, "c", "", "remote server domain, eg: aa.bb.cc") // TODO: socks5://127.0.0.1:1080 + flag.StringVar(&Client.Address, "c", "", "remote server domain, eg: aa.bb.cc, socks5h://127.0.0.1:1080") flag.StringVar(&Client.HTTPProxy.Address, "http_proxy", ":8080", "http proxy, empty to disable") flag.StringVar(&Client.DNS.ServeIP, "dns_ip", "", "upstream dns, eg: 127.0.0.1, disable dns proxy if empty") flag.StringVar(&Client.DNS.Upstream, "dns_upstream", "", "dns relay server ip, dynamic detect if empty") diff --git a/conf/conf_darwin.go b/conf/conf_darwin.go index 96513d3..2deeaa1 100644 --- a/conf/conf_darwin.go +++ b/conf/conf_darwin.go @@ -15,13 +15,13 @@ import ( "github.com/wweir/utils/log" ) -const svcPath = "/Library/LaunchDaemons/cc.wweir.sower.plist" +const svcPath = "/Library/LaunchDaemons/sower.plist" const svcFile = ` Label - cc.wweir.sower + sower ProgramArguments /bin/sh diff --git a/conf/dynamic_rule.go b/conf/dynamic_rule.go index b38abbc..b563d62 100644 --- a/conf/dynamic_rule.go +++ b/conf/dynamic_rule.go @@ -9,6 +9,7 @@ import ( "time" "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/internal/socks5" "github.com/wweir/sower/util" "github.com/wweir/utils/log" "github.com/wweir/utils/mem" @@ -78,15 +79,21 @@ func (d *dynamic) Get(key interface{}) (err error) { defer wg.Done() var conn net.Conn - if conn, err = tls.Dial("tcp", net.JoinHostPort(Client.Address, "443"), &tls.Config{}); err != nil { - log.Errorw("tls dial", "addr", net.JoinHostPort(Client.Address, "443"), "err", err) - return - } + if addr, ok := socks5.IsSocks5Schema(Client.Address); ok { + conn, err = net.Dial("tcp", addr) + conn = socks5.ToSocks5(conn, domain, uint16(ping.port)) - if ping.port == http.HTTP { - conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTP, "", 80) } else { - conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTPS, "", 443) + conn, err = tls.Dial("tcp", net.JoinHostPort(Client.Address, "443"), &tls.Config{}) + if ping.port == http.HTTP { + conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTP, "", 80) + } else { + conn = http.NewTgtConn(conn, passwordData, http.TGT_HTTPS, "", 443) + } + } + if err != nil { + log.Errorw("sower dial", "addr", Client.Address, "err", err) + return } if err := ping.port.PingWithConn(domain, conn, timeout); err != nil { diff --git a/conf/sower.toml b/conf/sower.toml index 8c6d0a6..a4ae296 100644 --- a/conf/sower.toml +++ b/conf/sower.toml @@ -1,5 +1,5 @@ [client] - address = "aa.bb.cc" + address = "" # aa.bb.cc, socks5h://127.0.0.1:1080 [client.dns] flush_cmd="" # macOS: pkill mDNSResponder || true, Windows: ipconfig /flushdnss diff --git a/go.mod b/go.mod index 7948dba..9525e21 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,5 @@ require ( github.com/pkg/errors v0.9.1 github.com/wweir/utils v0.0.0-20200214114658-f6f356a08736 golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 - golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe + golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 ) diff --git a/go.sum b/go.sum index bfbc677..c273591 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/internal/http/tgt_parser.go b/internal/http/tgt_parser.go index c2f3e46..9dafbce 100644 --- a/internal/http/tgt_parser.go +++ b/internal/http/tgt_parser.go @@ -72,16 +72,10 @@ func (c *conn) Write(b []byte) (n int, err error) { } // ParseAddr parse target addr from net.Conn -func ParseAddr(conn net.Conn, password []byte) (c net.Conn, domain string, port uint16, err error) { +func ParseAddr(conn net.Conn, password []byte) (_ net.Conn, domain string, port uint16, err error) { teeConn := &util.TeeConn{Conn: conn} teeConn.StartOrReset() - defer func() { - if err != nil { - teeConn.Close() - } else { - teeConn.Stop() - } - }() + defer teeConn.Stop() head := new(header) if err = binary.Read(conn, binary.BigEndian, head); err != nil { @@ -102,37 +96,40 @@ func ParseAddr(conn net.Conn, password []byte) (c net.Conn, domain string, port case TGT_HTTP: teeConn.DropAndRestart() - - var resp *http.Request - resp, err = http.ReadRequest(bufio.NewReader(teeConn)) - if err != nil { - return nil, "", 0, err - } - - idx := strings.LastIndex(resp.Host, ":") - if idx == -1 { - return teeConn, resp.Host, 80, nil - } - - var port uint64 - if port, err = strconv.ParseUint(resp.Host[idx+1:], 10, 16); err != nil { - return nil, "", 0, err - } - return teeConn, resp.Host[:idx], uint16(port), nil + return ParseHTTP(teeConn) case TGT_HTTPS: teeConn.DropAndRestart() - - domain, _, err := extractSNI(teeConn) - if err != nil { - return nil, "", 0, err - } - return teeConn, domain, head.Port, nil + conn, domain, err = ParseHTTPS(teeConn) + return conn, domain, head.Port, err default: - return nil, "", 0, errors.New("invalid request") + return teeConn, "", 0, errors.New("invalid request") } } +func ParseHTTP(teeConn net.Conn) (_ net.Conn, domain string, port uint16, err error) { + resp, err := http.ReadRequest(bufio.NewReader(teeConn)) + if err != nil { + return teeConn, "", 0, err + } + + idx := strings.LastIndex(resp.Host, ":") + if idx == -1 { + return teeConn, resp.Host, 80, nil + } + + p, err := strconv.ParseUint(resp.Host[idx+1:], 10, 16) + if err != nil { + return teeConn, "", 0, err + } + return teeConn, resp.Host[:idx], uint16(p), nil +} +func ParseHTTPS(teeConn net.Conn) (_ net.Conn, domain string, err error) { + if domain, _, err = extractSNI(teeConn); err != nil { + return nil, "", err + } + return teeConn, domain, nil +} var errChecksum = errors.New("invalid checksum") diff --git a/internal/socks5/rfc_def.go b/internal/socks5/rfc_def.go new file mode 100644 index 0000000..313bfee --- /dev/null +++ b/internal/socks5/rfc_def.go @@ -0,0 +1,44 @@ +package socks5 + +// https://tools.ietf.org/html/rfc1928 + +type authReq struct { + VER byte + NMETHODS byte + METHODS [1]byte // 1 to 255, fix to no authentication +} + +type authResp struct { + VER byte + METHOD byte +} + +type request struct { + req + DST_ADDR []byte // first byte is length + DST_PORT []byte // two bytes +} +type req struct { + VER byte + CMD byte + RSV byte + ATYP byte +} + +func (r *request) Bytes() []byte { + out := []byte{r.VER, r.CMD, r.RSV, r.ATYP} + out = append(out, r.DST_ADDR...) + return append(out, r.DST_PORT...) +} + +type response struct { + resp + DST_ADDR []byte // first byte is length + DST_PORT []byte // two bytes +} +type resp struct { + VER byte + REP byte + RSV byte + ATYP byte +} diff --git a/internal/socks5/socks5.go b/internal/socks5/socks5.go index b346fc1..a3a1aed 100644 --- a/internal/socks5/socks5.go +++ b/internal/socks5/socks5.go @@ -5,13 +5,28 @@ import ( "fmt" "io" "net" - "strconv" + "strings" ) -func ToSocks5(c net.Conn, domain, port string) net.Conn { - num, _ := strconv.Atoi(port) - bytes := []byte{byte(num >> 8), byte(num)} - return &conn{init: make(chan struct{}), Conn: c, domain: domain, port: bytes} +func IsSocks5Schema(addr string) (string, bool) { + if strings.HasPrefix(addr, "socks5://") { + return strings.TrimPrefix(addr, "socks5://"), true + } + + if strings.HasPrefix(addr, "socks5h://") { + return strings.TrimPrefix(addr, "socks5h://"), true + } + + return addr, false +} + +func ToSocks5(c net.Conn, domain string, port uint16) net.Conn { + return &conn{ + init: make(chan struct{}), + Conn: c, + domain: domain, + port: []byte{byte(port >> 8), byte(port)}, + } } type conn struct { @@ -106,44 +121,3 @@ func (c *conn) Write(b []byte) (n int, err error) { close(c.init) return c.Conn.Write(b) } - -type authReq struct { - VER byte - NMETHODS byte - METHODS [1]byte // 1 to 255, fix to no authentication -} - -type authResp struct { - VER byte - METHOD byte -} - -type request struct { - req - DST_ADDR []byte // first byte is length - DST_PORT []byte // two bytes -} -type req struct { - VER byte - CMD byte - RSV byte - ATYP byte -} - -func (r *request) Bytes() []byte { - out := []byte{r.VER, r.CMD, r.RSV, r.ATYP} - out = append(out, r.DST_ADDR...) - return append(out, r.DST_PORT...) -} - -type response struct { - resp - DST_ADDR []byte // first byte is length - DST_PORT []byte // two bytes -} -type resp struct { - VER byte - REP byte - RSV byte - ATYP byte -} diff --git a/proxy/dns.go b/proxy/dns.go index 4a65b0d..3572ca4 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -47,7 +47,7 @@ func ServeDNS(redirectIP, relayServer string) { server, err := pickRelayAddr(relayServer) if err != nil { log.Errorw("detect upstream dns", "err", err) - } else { + } else if relayServer != server { relayServer = server log.Infow("detect upstream dns", "addr", relayServer) } diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index 670372f..67f552b 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -11,6 +11,7 @@ import ( "github.com/wweir/sower/conf" _http "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/util" "github.com/wweir/utils/log" ) @@ -33,20 +34,12 @@ func startHTTPProxy(httpProxyAddr, serverAddr string, password []byte) { } func httpProxy(w http.ResponseWriter, r *http.Request, serverAddr string, password []byte) { - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - host = r.Host - } + host, port := util.ParseHostPort(r.Host, 80) roundTripper := &http.Transport{} if conf.ShouldProxy(host) { roundTripper.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - conn, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) - if err != nil { - return nil, err - } - - return _http.NewTgtConn(conn, password, _http.TGT_HTTP, "", 80), nil + return dial(serverAddr, password, _http.TGT_HTTP, host, port) } } @@ -67,17 +60,7 @@ func httpProxy(w http.ResponseWriter, r *http.Request, serverAddr string, passwo } func httpsProxy(w http.ResponseWriter, r *http.Request, serverAddr string, password []byte) { - var host string - var port = uint16(443) - if h, p, err := net.SplitHostPort(r.Host); err != nil { - host = r.Host - } else if pNum, err := strconv.ParseUint(p, 10, 16); err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - return - } else { - host = h - port = uint16(pNum) - } + host, port := util.ParseHostPort(r.Host, 443) conn, _, err := w.(http.Hijacker).Hijack() if err != nil { @@ -94,21 +77,14 @@ func httpsProxy(w http.ResponseWriter, r *http.Request, serverAddr string, passw var rc net.Conn if conf.ShouldProxy(host) { - rc, err = tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) - if err != nil { - conn.Write([]byte("sower: tls dial: " + err.Error())) - conn.Close() - return - } - rc = _http.NewTgtConn(rc, password, _http.TGT_HTTPS, "", port) - + rc, err = dial(serverAddr, password, _http.TGT_HTTPS, host, port) } else { rc, err = net.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(int(port)))) - if err != nil { - conn.Write([]byte("sower: tcp dial: " + err.Error())) - conn.Close() - return - } + } + if err != nil { + conn.Write([]byte("sower dial " + serverAddr + " fail: " + err.Error())) + conn.Close() + return } defer rc.Close() diff --git a/proxy/proxy.go b/proxy/proxy.go index 45c627d..2918023 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -7,6 +7,8 @@ import ( "strconv" _http "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/internal/socks5" + "github.com/wweir/sower/util" "github.com/wweir/utils/log" "golang.org/x/crypto/acme/autocert" ) @@ -20,6 +22,8 @@ type head struct { func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards map[string]string) { passwordData := []byte(password) + _, isSocks5 := socks5.IsSocks5Schema(serverAddr) + if httpProxy != "" { go startHTTPProxy(httpProxy, serverAddr, passwordData) } @@ -39,15 +43,30 @@ func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards ma go func(conn net.Conn) { defer conn.Close() + if isSocks5 { + teeConn := &util.TeeConn{Conn: conn} + teeConn.StartOrReset() + switch tgtType { + case _http.TGT_HTTP: + conn, host, port, err = _http.ParseHTTP(teeConn) + case _http.TGT_HTTPS: + conn, host, err = _http.ParseHTTPS(teeConn) + } + if err != nil { + log.Errorw("parse socks5 target", "err", err) + return + } + teeConn.Stop() + } - rc, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) + rc, err := dial(serverAddr, passwordData, tgtType, host, port) if err != nil { - log.Errorw("tls dial", "addr", net.JoinHostPort(serverAddr, "443"), "err", err) + log.Errorw("dial", "addr", serverAddr, "err", err) return } defer rc.Close() - relay(conn, _http.NewTgtConn(rc, passwordData, tgtType, host, port)) + relay(conn, rc) }(conn) } } @@ -59,16 +78,7 @@ func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards ma for from, to := range forwards { go func(from, to string) { - host, portStr, err := net.SplitHostPort(to) - if err != nil { - log.Fatalw("parse port forward", "target", to, "err", err) - } - portNum, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - log.Fatalw("parse port forward", "target", to, "err", err) - } - port := uint16(portNum) - + host, port := util.ParseHostPort(to, 0) relayToRemote(_http.TGT_OTHER, from, host, port) }(from, to) } diff --git a/proxy/util.go b/proxy/util.go index efffeaa..570e7d8 100644 --- a/proxy/util.go +++ b/proxy/util.go @@ -1,13 +1,33 @@ package proxy import ( + "crypto/tls" "io" "net" "sync" "sync/atomic" "time" + + "github.com/wweir/sower/internal/http" + "github.com/wweir/sower/internal/socks5" ) +func dial(serverAddr string, password []byte, tgtType byte, domain string, port uint16) (net.Conn, error) { + if addr, ok := socks5.IsSocks5Schema(serverAddr); ok { + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + return socks5.ToSocks5(conn, domain, port), nil + } + + conn, err := tls.Dial("tcp", net.JoinHostPort(serverAddr, "443"), &tls.Config{}) + if err != nil { + return nil, err + } + return http.NewTgtConn(conn, password, tgtType, domain, port), nil +} + func relay(conn1, conn2 net.Conn) { wg := &sync.WaitGroup{} exitFlag := new(int32) diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..5e6607a --- /dev/null +++ b/util/util.go @@ -0,0 +1,19 @@ +package util + +import ( + "net" + "strconv" +) + +func ParseHostPort(addr string, defaultPort uint16) (string, uint16) { + h, p, err := net.SplitHostPort(addr) + if err != nil { + if defaultPort == 0 { + panic("parse port fail with no default, addr: " + addr) + } + return addr, defaultPort + } + + pNum, _ := strconv.ParseUint(p, 10, 16) + return h, uint16(pNum) +} From 899d64a92cd552c9a9379d235b492b126424ee28 Mon Sep 17 00:00:00 2001 From: wweir Date: Mon, 17 Feb 2020 06:56:34 +0800 Subject: [PATCH 6/6] Update readme --- README.md | 174 +++++++++++++++++++------------------------------ main.go | 2 +- proxy/dns.go | 2 +- proxy/proxy.go | 2 + 4 files changed, 72 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index cbba022..f84f2fb 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,79 @@ [![GitHub issue](https://img.shields.io/github/issues/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/issues) [![GitHub star](https://img.shields.io/github/stars/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/stargazers) [![GitHub license](https://img.shields.io/github/license/wweir/sower.svg?style=popout)](LICENSE) + + +中文介绍见 [Wiki](https://github.com/wweir/sower/wiki) + +The sower is a cross-platform intelligent transparent proxy tool. + +The first time you visit a new website, the sower will detect if the domain in the block list and add it in the dynamic detect list. So, you do not need to care about the rules, sower will handle it in an intelligent way. + +Sower provider both http_proxy/https_proxy and dns-based proxy. All these kinds of proxy support intelligent router. You can also port-forward any tcp request to remote, such as: ssh / smtp / pop3. + +You are able to enjoy it by setting http_proxy or your DNS without any other settings. + +If you already have another proxy solution, you can use it's socks5(h) service as parent proxy to enjoy sower's intelligent router. + + +## Installation +To enjoy the sower, you need to deploy sower on both server-side and client-side. + +Installation script has been integrated into sower. You can install sower as system service by running `./sower -install 'xxx'` + +## Server +*If you already have another proxy solution with socks5h support, you can skip server side.* + +At server-side, sower run just like a web server proxy. +It redirect http request to https, and proxy https requests to the setted upstream http service. +You can use your own certificate or use the auto generated certificate by sower. + +What you must set is the upstream http service. You can set it by parameter `-s`, eg: +``` shell +# sower -s 127.0.0.1:8080 +``` + +## Client +The easiest way to run it is: +``` shell +# sower -c aa.bb.cc # the `aa.bb.cc` can also be `socks5h://127.0.0.1:1080` +``` +But a configuration file is recommended to persist dynamic rules in client side. + +There are 3 kinds of proxy solutions, they are: http(s)_proxy / dns-based proxy / port-forward. + +### HTTP(S)_PROXY +An http(s)_proxy listening on `:8080` is setted by deault if you run sower as client mode. + +### dns-based proxy +You can set the `serve_ip` field in `dns` section in configuration file to start dns-based proxy. You should also set the value of `serve_ip` as your default DNS in OS. + +If you want to enjoy the full experience provided by sower, you can take sower as your private DNS on long running server and setting it as your default DNS in you router. + +### port-forward +The port-forward can be only setted in configuration file, you can set it in section `client.router.port_mapping`, eg: +``` toml +[client.router.port_mapping] +":2222"="aa.bb.cc:22" +``` + + +## Architecture ``` relay <--+ +-> target http service | | service +-------+-------+----+ | sower server | - +----^-----^---------+ - 80 443 -301 http -+ +----- https -to https | service - | - protected - by tls - dns <---+ | +--> direct - relay | | | request - +----+-----+----+----+ + +----^-------^-------+ + 80 443 +301 http -+ +----- https +to https | service + protected + by tls + socks5 | + dns <---+ ^ | +--> direct +relay | | | | request + +---+---+----+--+----+ | sower client | +----^--^----^---^---+ | | | | @@ -27,99 +85,3 @@ to https | service 80 http(s) forward 443 proxy ``` -# **In refactor** - -中文介绍见 [Wiki](https://github.com/wweir/sower/wiki) - -The sower is a cross-platform intelligent transparent proxy tool base on DNS solution. - -The first time you visit a new website, sower will detect if the domain in block list and add it in suggect list. So that, you do not need to care about the rules, sower will handle it in a intelligent way. - -If you wanna enjoy the sower, you need to deploy sower on both server and client side. -On client side, sower listening UDP `53` and TCP `80`/`443` ports, so that you need run it with privileged. -On server side, it just listening to a port (default `5533`), parse and relay the request to target server. - -Sower also provides an http(s) proxy listening on `:8080` by default. You can turn it off or use another port at any time. - - -## Installation -After Deployed, please check your config file, it is placed in `/usr/local/etc/sower.toml` by default. Here is the example config file [**conf/sower.toml**](https://github.com/wweir/sower/blob/master/conf/sower.toml) - -### Auto deploy -Auto deploy script support Linux server side and masOS/Linux client side. - -```shell -$ bash -c "$(curl -sL https://git.io/JeZzX)" -``` - -Then modify the configuration file as needed and set `127.0.0.1` as your first domain name server. -In most situation, you just need to modify `/etc/resolv.conf`. - -If you wanna uninstall sower, run: - -```shell -$ bash -c "$(curl -sL https://git.io/JeZz1)" -``` - -### Manually deploy -1. Download the precompiled file from https://github.com/wweir/sower/releases -2. Decompression the file into a folder -3. Run `./sower -h` for help -5. Config domain name server -4. Config auto start - -### Docker deploy -The auto build docker images are [wweir/sower](https://hub.docker.com/r/wweir/sower). - -It is very simple to use it on the server side. Export the port(5533) and run it directly. - -But the client is more troublesome and needs some understanding of the working mechanism of the sower. - - -## Architecture -``` - request target servers -<-------------+ +-------------> - | | - | | - +------------server-------------+ - | | relay service| | - | +-----+---------------------+ | - | | | | - | | parse http(s) target url | | - | | | | - | +---------------------------+ | - | shadow service | - +--------^----------------------+ - | request domain server - quic / KCP / TCP +----------> - | | - +--------+---client+------+-----+ - | | | - | shadow service | | - | relay service | dns | - | | service | - | | | - | 127.0.0.1 or other | - | | | - +-^-----^----------+---^----^---+ - | | | | - | | | | +-----> -http(s) proxy | +----------+ | | - 2 1 1 2 - + + + + - blocked request normal request - -``` -For more detail, see [透明代理 Sower 技术剖析](https://wweir.cc/post/%E9%80%8F%E6%98%8E%E4%BB%A3%E7%90%86-sower-%E6%8A%80%E6%9C%AF%E5%89%96%E6%9E%90/) - - -## Todo -- [x] authenticate -- [ ] broker(waiting for QUIC implementation to be stable) -- [x] CI/CD -- [x] relay optimization -- [x] deploy script for all normal platform -- [x] dns rule intelligent suggestions -- [x] use socks5 as upstream proxy -- [ ] multi port http_proxy support diff --git a/main.go b/main.go index f0ba5b3..7ae316c 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ func main() { if conf.Client.Address != "" { if conf.Client.DNS.ServeIP != "" { - go proxy.ServeDNS(conf.Client.DNS.ServeIP, conf.Client.DNS.Upstream) + go proxy.StartDNS(conf.Client.DNS.ServeIP, conf.Client.DNS.Upstream) } proxy.StartClient(conf.Password, conf.Client.Address, conf.Client.HTTPProxy.Address, diff --git a/proxy/dns.go b/proxy/dns.go index 3572ca4..df721fb 100644 --- a/proxy/dns.go +++ b/proxy/dns.go @@ -11,7 +11,7 @@ import ( "github.com/wweir/utils/log" ) -func ServeDNS(redirectIP, relayServer string) { +func StartDNS(redirectIP, relayServer string) { serveIP := net.ParseIP(redirectIP) if redirectIP == "" || serveIP.String() != redirectIP { log.Fatalw("invalid listen ip", "ip", redirectIP) diff --git a/proxy/proxy.go b/proxy/proxy.go index 2918023..5becb09 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -43,9 +43,11 @@ func StartClient(password, serverAddr, httpProxy, dnsServeIP string, forwards ma go func(conn net.Conn) { defer conn.Close() + if isSocks5 { teeConn := &util.TeeConn{Conn: conn} teeConn.StartOrReset() + switch tgtType { case _http.TGT_HTTP: conn, host, port, err = _http.ParseHTTP(teeConn)