From 1a24286bbc5136ee03262d85aa305120b787a182 Mon Sep 17 00:00:00 2001 From: wweir Date: Sat, 15 Feb 2020 10:49:57 +0800 Subject: [PATCH] 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