Refactor to tls

This commit is contained in:
wweir
2020-02-15 15:52:15 +08:00
parent fd5c34dca1
commit 1a24286bbc
31 changed files with 825 additions and 507 deletions
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -8,7 +8,7 @@
<array>
<string>/usr/local/bin/sower</string>
<string>-f</string>
<string>/usr/local/etc/sower.toml</string>
<string>/etc/sower/sower.toml</string>
</array>
<key>KeepAlive</key>
<true/>
-127
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
-20
View File
@@ -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
+1 -1
View File
@@ -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 ./...
+2
View File
@@ -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.
+83 -77
View File
@@ -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
}
+3 -3
View File
@@ -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
}
+4 -3
View File
@@ -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
}
+4 -5
View File
@@ -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
}
+132
View File
@@ -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:
}
}
+22 -11
View File
@@ -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",
+11 -100
View File
@@ -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
}
-20
View File
@@ -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)
+3 -2
View File
@@ -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
)
+6 -5
View File
@@ -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=
+145
View File
@@ -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
}
+66
View File
@@ -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)
}
}
+1 -1
View File
@@ -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"
+15
View File
@@ -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)
}
+15
View File
@@ -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)
}
+9 -75
View File
@@ -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()
}
-42
View File
@@ -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
}
+115
View File
@@ -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)
}
+138
View File
@@ -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)
}
}
+31
View File
@@ -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()
}
+5 -5
View File
@@ -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)
}
+6 -6
View File
@@ -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},
+4
View File
@@ -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