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 @@
[](https://github.com/wweir/sower/stargazers)
[](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