Compare commits

...
69 Commits
Author SHA1 Message Date
wweir 9bc5a32074 Add more fuzz match support @release 2019-01-29 13:34:52 +08:00
wweir 8194083f54 Add more fuzz rules support @release 2019-01-28 22:41:07 +08:00
wweir c20903137d Fix #5, add ipv6 support @release 2019-01-28 16:54:27 +08:00
wweir 2f3c98b8d3 Clean code 2019-01-27 08:35:12 +08:00
wweir bc99bda1b3 Clear unnessary block rule 2019-01-25 23:13:48 +08:00
wweir ceedef4203 Add https ping @release 2019-01-25 22:46:28 +08:00
wweir 712b689708 Force enable go modules 2019-01-25 16:13:22 +08:00
wweir d5260d66df Fix match rule 2019-01-25 15:27:56 +08:00
wweir bde27f64ec Fix 433 detect 2019-01-23 22:31:37 +08:00
wweir 04193eb608 Change intelligent detect logic @release 2019-01-23 20:10:41 +08:00
wweir 41a1adc707 Fix intelligent detect logic @release 2019-01-23 12:52:34 +08:00
wweir c9191ec928 Fix intelligent detect logic @release 2019-01-23 11:36:34 +08:00
wweir 14da5b711e Fix intelligent logic @release 2019-01-23 07:33:28 +08:00
wweir 59257c0673 Add description about intelligent detect @release 2019-01-22 22:25:45 +08:00
wweir 7db92a19be Fix intelligent detect logic @release 2019-01-22 22:08:59 +08:00
wweir fbae2999dc Change deploy script to keep suggestions @release 2019-01-22 21:24:33 +08:00
wweir 92991da6a0 Fix rule for github network speed 2019-01-22 21:16:25 +08:00
wweir b60b5fceb3 Fix deadloop in wildcard dns setting case @release 2019-01-22 20:54:27 +08:00
wweir 756a8c8d17 Add intelligent rule suggestions @release 2019-01-22 20:12:47 +08:00
wweir e02da26936 Refactor for intelligent dns rule 2019-01-21 13:55:55 +08:00
wweir c5adcc4421 Fix permission @release 2019-01-21 11:22:38 +08:00
wweir c20c3621c6 Change release filenames @release 2019-01-21 11:19:31 +08:00
wweir 055c1a2901 Fix conn leak @release 2019-01-21 10:57:48 +08:00
wweir fc2ea8b8c0 Update default config file path 2019-01-20 21:41:29 +08:00
wweir 3e6b9e96ca Add docs link 2019-01-20 17:47:57 +08:00
wweir 2264d73c45 Add docker deploy description 2019-01-19 18:52:34 +08:00
wweir 99ba06b2d4 Add auto deploy and undeploy description 2019-01-19 18:40:02 +08:00
wweir a8e026ff7f For user friendly @release 2019-01-19 16:07:55 +08:00
wweir 9e377e4a2d Add deploy script for macOS and Linux platform 2019-01-19 15:33:00 +08:00
wweir 107174fb6b Fix badge link 2019-01-19 12:55:57 +08:00
wweir 3f8f5c2ef1 Fix badge display 2019-01-19 12:43:21 +08:00
wweir 918b964b78 Fix CI docker image tag 2019-01-19 12:37:05 +08:00
wweir 34722a460c Fix docker image and add release toggle option 2019-01-19 12:13:12 +08:00
wweir 8734486c48 Add CI release support 2019-01-19 11:58:33 +08:00
wweir 8d0ee2a9ea Add ci scripts 2019-01-19 11:03:32 +08:00
wweir 98247845ad Clean code 2019-01-19 08:10:11 +08:00
wweir 72dacaf1de Fix telegram config 2019-01-16 23:24:10 +08:00
wweir 56d5090841 Separate http_proxy interface setting for some case 2019-01-16 22:37:18 +08:00
wweir 821cbc302e Add http proxy support 2019-01-16 18:42:03 +08:00
wweir f6eddb9f96 For #1, support for specified a network interface 2019-01-16 14:40:06 +08:00
wweir 344531c301 Clean code 2019-01-16 07:26:59 +08:00
wweir df897f72c6 Fix quic session close 2019-01-15 22:55:50 +08:00
wweir dd2fe95900 update readme architecture 2019-01-15 19:15:06 +08:00
wweir 08a5d4363b Fix dead loop 2019-01-15 19:06:16 +08:00
wweir e712dcb78c Add multi cipher support 2019-01-15 19:00:12 +08:00
wweir e9f5fc4789 Extend quic idle timeout 2019-01-15 08:26:06 +08:00
wweir f76f31d054 Update dependency 2019-01-15 08:25:48 +08:00
wweir e0de537042 Clean dirty code 2019-01-15 08:04:54 +08:00
wweir b050e312b2 Redesign shadow payload 2019-01-12 14:32:44 +08:00
wweir d78a757200 Fix shadow conn 2019-01-12 13:53:13 +08:00
wweir b2282a58a2 Fix relay logic 2019-01-10 08:02:04 +08:00
wweir 0fb2271cac Fix shadow 2019-01-10 07:54:44 +08:00
wweir a8089af235 Fix shadow 2019-01-07 22:28:32 +08:00
wweir 6e4a6c794e Fix shadow 2019-01-06 12:17:01 +08:00
wweir 1d620b8191 Fix shadow conn 2019-01-05 10:02:50 +08:00
wweir 9586e7bc8e Test close conn 2019-01-04 16:55:59 +08:00
wweir ec4286e6d4 Refactor crypto 2019-01-04 16:14:15 +08:00
wweir c01f217939 Fix crypto 2019-01-04 15:19:56 +08:00
wweir c62e3a3284 Add crypto support 2019-01-04 14:34:00 +08:00
wweir 578d9162aa Add tcp support 2018-12-17 07:41:34 +08:00
wweir 002ac49e65 Add password flag 2018-12-16 23:25:51 +08:00
wweir ee47f02cb5 Add kcp support 2018-12-16 23:14:58 +08:00
wweir b1127e8f9e Fix generate command 2018-12-16 21:30:57 +08:00
wweir 282969b696 Refactor: refactor proxy 2018-12-16 21:25:48 +08:00
wweir 2daaa8f3ef Close session on peer going away error 2018-12-13 22:27:40 +08:00
wweir d6776345a0 Update idle timeout 2018-12-09 12:20:34 +08:00
wweir 0e03605c37 Support clear dns cache hook 2018-12-09 10:34:53 +08:00
wweir 9ca58982b7 Update quic dial config 2018-12-08 19:29:39 +08:00
wweir 60452da6b1 Support dns query with port 2018-12-08 10:38:57 +08:00
44 changed files with 1909 additions and 535 deletions
+51
View File
@@ -0,0 +1,51 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.11.4
steps:
- checkout # check out the code in the project directory
- setup_remote_docker # install docker in docker
- run:
name: Prepare Environment
command: |
cp conf/sower.toml .
cp deploy/* .
mkdir artifacts
- run:
name: build linux Binary
command: |
make build
tar czvf artifacts/sower_linux_amd64.tar.gz sower sower.toml sower-client.service sower-server.service
- run:
name: build macOS Binary
command: |
GOOS=darwin GOARCH=amd64 make build
tar czvf artifacts/sower_darwin_amd64.tar.gz sower sower.toml cc.wweir.sower.plist
- run:
name: build Windows Binary
command: |
GOOS=windows GOARCH=amd64 make build
tar czvf artifacts/sower_windows_amd64.tar.gz sower sower.toml
- run:
name: Publish Release on GitHub
command: |
if git log -1 --pretty='%s' | grep -oE '(^|\s)@release(\s|$)'; then
GHR_VERSION="$(curl -s "https://api.github.com/repos/tcnksm/ghr/releases/latest" | awk -F '"' '/tag_name/{printf $4}')"
curl -SLf https://github.com/tcnksm/ghr/releases/download/"$GHR_VERSION"/ghr_"$GHR_VERSION"_linux_amd64.tar.gz | tar xzC /go/bin --strip-components=1
VERSION=$(git describe --tags)
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION%-*-*} artifacts/
fi
- run:
name: build docker image
command: |
if git log -1 --pretty='%s' | grep -oE '(^|\s)@release(\s|$)'; then
docker login -u $DOCKER_USER -p $DOCKER_PASS
make image
VERSION=$(git describe --tags)
docker tag sower wweir/sower:${VERSION%-*-*}
docker push wweir/sower:${VERSION%-*-*}
docker tag sower wweir/sower:latest
docker push wweir/sower:latest
fi
+4 -5
View File
@@ -12,11 +12,10 @@
*.out
.DS_Store
**/.DS_Store
._*
*.swp
*.swo
.vscode
.idea
sower
cert.pem
**/cert.pem
privkey.pem
**/privkey.pem
/sower.toml
+18 -6
View File
@@ -1,19 +1,31 @@
SERVER:=127.0.0.1:5533
default: build
default: test build
generate:
go generate ./...
test:
go test -race ./...
build:
GOBIN=$(PWD) go install -v
GO111MODULE=on go build -v -ldflags \
"-X main.version=$(shell git describe --tags) \
-X main.date=$(shell date +%Y-%m-%d)"
image:
docker build -t sower -f deploy/Dockerfile .
kill:
sudo pkill -9 sower || true
client: build kill
sudo $(PWD)/sower -f conf/sower.toml -logtostderr
sudo $(PWD)/sower -f conf/sower.toml
server: build kill
$(PWD)/sower -logtostderr -v 1
$(PWD)/sower -n TCP -v 1
run: build kill
$(PWD)/sower -logtostderr -v 1 &
sudo $(PWD)/sower -f conf/sower.toml -logtostderr
$(PWD)/sower -n TCP -v 1 &
sudo $(PWD)/sower -f conf/sower.toml &
@sleep 1
curl 127.0.0.1
@sleep 1
@sudo pkill -9 sower || true
+63 -20
View File
@@ -1,7 +1,50 @@
# sower
Yet another cross platform transparent proxy tool
[![GitHub release](http://img.shields.io/github/release/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/releases)
[![CircleCI](https://img.shields.io/circleci/token/64114421aab286b6e918741071535250a5273f06/project/github/wweir/sower/master.svg?style=popout)](https://circleci.com/gh/wweir/sower)
[![GitHub issue](https://img.shields.io/github/issues/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/issues)
[![GitHub star](https://img.shields.io/github/stars/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/stargazers)
[![GitHub license](https://img.shields.io/github/license/wweir/sower.svg?style=popout)](LICENSE)
## architecture
The sower is a cross-platform intelligent transparent proxy tool base on DNS solution.
The first time you visit a new website, sower will detect if the domain in block list and add it in suggect list. So that, you do not need to care about the rules, sower will handle it in a intelligent way.
If you wanna enjoy the sower, you need to deploy sower on both server and client side.
On client side, sower listening UDP `53` and TCP `80`/`443` ports, so that you need run it with privileged.
On server side, it just listening to a port (default `5533`), parse and relay the request to target server.
Sower also provides an http(s) proxy listening on `:8080` by default. You can turn it off or use another port at any time.
## Installation
### Auto deploy
Auto deploy script support Linux server side and masOS/Linux client side.
```shell
$ bash -c "$(curl -s https://raw.githubusercontent.com/wweir/sower/master/deploy/install)"
```
Then modify the configuration file as needed and set `127.0.0.1` as your first domain name server.
In most situation, you just need to modify `/etc/resolv.conf`.
If you wanna uninstall sower, change `install` into `uninstall` and rerun the command.
### Manually deploy
1. Download the precompiled file from https://github.com/wweir/sower/releases
2. Decompression the file into a folder
3. Run `./sower -h` for help
5. Config domain name server
4. Config auto start
### Docker deploy
The auto build docker images are [wweir/sower](https://hub.docker.com/r/wweir/sower).
It is very simple to use it on the server side. Map the port and run it directly.
But the client is more troublesome and needs some understanding of the working mechanism of the sower.
## Architecture
```
request target servers
<-------------+ +------------->
@@ -11,38 +54,38 @@ Yet another cross platform transparent proxy tool
| | relay service| |
| +-----+---------------------+ |
| | | |
| | parsr http(s) target url | |
| | parse http(s) target url | |
| | | |
| +---------------------------+ |
| |
| shadow service |
+--------^----------------------+
| request domain server
quic connection +---------->
quic / KCP / TCP +---------->
| |
+--------+---client+------+-----+
| | |
| | |
| shadow service | |
| relay service | dns |
| | server |
| | service |
| | |
| 127.0.0.1 or other |
| | |
+-------^----------+---^----^---+
| | |
| | | +----->
| +----------+ | |
+-^-----^----------+---^----^---+
| | | |
| | | | +----->
http(s) proxy | +----------+ | |
2 1 1 2
+ + + +
block request normal request
blocked request normal request
```
For more detail, see [透明代理 Sower 技术剖析](https://wweir.cc/post/%E9%80%8F%E6%98%8E%E4%BB%A3%E7%90%86-sower-%E6%8A%80%E6%9C%AF%E5%89%96%E6%9E%90/)
## install
1. install server on `server node` by `make server`
2. write config file, example: [conf/sower.toml](https://github.com/wweir/sower/blob/master/conf/sower.toml)
3. install client on `client node` by `make client`
4. add `127.0.0.1` as you first domain server
## todo
- [ ] authenticate
- [ ] broker
## Todo
- [x] authenticate
- [ ] broker
- [x] CI/CD
- [ ] relay optimization
- [ ] deploy script for all normal platform
- [x] dns rule intelligent suggestions
+99 -27
View File
@@ -1,65 +1,106 @@
package conf
import (
"context"
"flag"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/BurntSushi/toml"
"github.com/golang/glog"
toml "github.com/pelletier/go-toml"
"github.com/wweir/fsnotify"
"github.com/wweir/sower/util"
)
var Conf = struct {
ConfigFile string
ServerPort string `toml:"server_port"`
ServerAddr string `toml:"server_addr"`
DnsServer string `toml:"dns_server"`
ClientIP string `toml:"client_ip"`
ClientIPNet net.IP `toml:"-"`
ConfigFile string
NetType string `toml:"net_type"`
Cipher string `toml:"cipher"`
Password string `toml:"password"`
ServerPort string `toml:"server_port"`
ServerAddr string `toml:"server_addr"`
HTTPProxy string `toml:"http_proxy"`
DNSServer string `toml:"dns_server"`
ClientIP string `toml:"client_ip"`
ClearDNSCache string `toml:"clear_dns_cache"`
BlockList []string `toml:"blocklist"`
WhiteList []string `toml:"whitelist"`
Suggestions []string `toml:"suggestions"`
Verbose int `toml:"verbose"`
}{}
var mu = &sync.Mutex{}
var OnRefreash = []func() error{
func() (err error) {
mu.Lock()
defer mu.Unlock()
f, err := os.OpenFile(Conf.ConfigFile, os.O_RDONLY, 0644)
if err != nil {
return err
}
defer f.Close()
file := Conf.ConfigFile
if err = toml.NewDecoder(f).Decode(&Conf); err != nil {
return err
}
Conf.ConfigFile = file
return flag.Set("v", strconv.Itoa(Conf.Verbose))
},
func() error {
if Conf.ClearDNSCache != "" {
ctx, _ := context.WithTimeout(context.TODO(), 5*time.Second)
if err := exec.CommandContext(ctx, "sh", "-c", Conf.ClearDNSCache).Run(); err != nil {
glog.Errorln(err)
}
}
return nil
},
}
func init() {
flag.StringVar(&Conf.ConfigFile, "f", "", "config file location")
flag.StringVar(&Conf.ConfigFile, "f", filepath.Dir(os.Args[0])+"/sower.toml", "config file location")
flag.StringVar(&Conf.NetType, "n", "TCP", "proxy net type (QUIC|KCP|TCP)")
flag.StringVar(&Conf.Cipher, "C", "AES_128_GCM", "cipher type (AES_128_GCM|AES_192_GCM|AES_256_GCM|CHACHA20_IETF_POLY1305|XCHACHA20_IETF_POLY1305)")
flag.StringVar(&Conf.Password, "p", "12345678", "password")
flag.StringVar(&Conf.ServerPort, "P", "5533", "server mode listen port")
flag.StringVar(&Conf.ServerAddr, "s", "", "server IP (run in client mode if set)")
flag.StringVar(&Conf.DnsServer, "d", "114.114.114.114", "client dns server")
flag.StringVar(&Conf.HTTPProxy, "H", "", "http proxy listen addr")
flag.StringVar(&Conf.DNSServer, "d", "114.114.114.114", "client dns server")
flag.StringVar(&Conf.ClientIP, "c", "127.0.0.1", "client dns service redirect IP")
if !flag.Parsed() {
flag.Set("logtostderr", "true")
flag.Parse()
}
if Conf.ConfigFile == "" {
if _, err := os.Stat(Conf.ConfigFile); os.IsNotExist(err) {
glog.Warningln("no config file has been load:", Conf.ConfigFile)
return
}
if err := OnRefreash[0](); err != nil {
panic(err)
for i := range OnRefreash {
if err := OnRefreash[i](); err != nil {
glog.Fatalln(err)
}
}
watchConfigFile()
}
var OnRefreash = []func() error{func() error {
if _, err := toml.DecodeFile(Conf.ConfigFile, &Conf); err != nil {
return err
}
Conf.ClientIPNet = net.ParseIP(Conf.ClientIP)
// for glog
if err := flag.Set("v", strconv.Itoa(Conf.Verbose)); err != nil {
return err
}
return nil
}}
// watchConfigFile changes, fsnotify take too much cpu time, DIY
func watchConfigFile() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
glog.Fatalln(err)
}
if err := watcher.Add(Conf.ConfigFile); err != nil {
if err := watcher.Add(filepath.Dir(Conf.ConfigFile)); err != nil {
glog.Fatalln(err)
}
@@ -67,6 +108,12 @@ func watchConfigFile() {
for {
select {
case event := <-watcher.Events:
if event.Op == fsnotify.Rename {
if err := watcher.Add(Conf.ConfigFile); err != nil {
glog.Errorln(err)
}
}
glog.Infof("watch %s event: %v", Conf.ConfigFile, event)
for i := range OnRefreash {
if err := OnRefreash[i](); err != nil {
@@ -79,3 +126,28 @@ func watchConfigFile() {
}
}()
}
func AddSuggest(domain string) {
mu.Lock()
defer mu.Unlock()
Conf.Suggestions = append(Conf.Suggestions, domain)
Conf.Suggestions = util.NewReverseSecSlice(Conf.Suggestions).Sort().Uniq()
// safe write
f, err := os.OpenFile(Conf.ConfigFile+"~", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
glog.Errorln(err)
return
}
defer f.Close()
if err := toml.NewEncoder(f).ArraysWithOneElementPerLine(true).Encode(Conf); err != nil {
glog.Errorln(err)
return
}
if err = os.Rename(Conf.ConfigFile+"~", Conf.ConfigFile); err != nil {
glog.Errorln(err)
}
}
+30 -67
View File
@@ -1,70 +1,33 @@
net_type="TCP"
cipher="AES_128_GCM"
password="12345678"
server_port="5533"
server_addr="127.0.0.1:5533" # replce it to server "IP"
server_addr="remote-server:5533" # replce it to remote server
dns_server="114.114.114.114"
# client_ip="192.168.0.1"
# server_addr="remote-server:5533" # replce it to remote server
http_proxy=":8080" # eg: 192.168.0.2:8080
dns_server="223.5.5.5" # Alibaba public dns
client_ip="127.0.0.1" # listen the IP, dns target is the IP
# clear_dns_cache="killall -HUP mDNSResponder"
blocklist=[
"*.google.com", # google
"ocsp.pki.goog",
"g.co",
"*.goo.gl",
"*.googleusercontent.com",
"*.googleapis.com",
"gmail.com",
"*.youtube.com", # youtube
"*.ytimg.com",
"*.ggpht.com",
"*.googlevideo.com",
"*.vimeo.com",
"*.facebook.com", # facebook
"*.xx.fbcdn.net",
"connect.facebook.net",
"*.twitter.com", # twitter
"t.co",
"*.twimg.com",
"www.linkedin.com",
"www.reddit.com", # reddit
"www.redditmedia.com",
"*.blogspot.com", # blogspot
"www.blogger.com",
"www.feedburner.com",
"img1.blogblog.com",
"*.aws.amazon.com", # amazon
"m.media-amazon.com",
"*.awsstatic.com",
"*.cloudfront.net", # atlassian
"*.medium.com",
"accounts-static.cdn.mozilla.net", # firefox
"*.services.mozilla.com",
"pocket-image-cache.com", # pocket
"*.golang.org", # golang
"go.googlesource.com",
"godoc.org",
"gist.github.com", # github
"*.k8s.io", # k8s
"k8s.gcr.io",
"*.wikipedia.org", # wikipeida
"*.wikimedia.org",
"*.wikisource.org",
"lookup-api.apple.com",
"lookup-api.apple.com.edgekey.net",
"e16991.b.akamaiedge.net",
"*.web.telegram.org", # telegram
"core.telegram.org",
"telegram.me",
"t.me",
"*.slack.com", #slack
"*.slack-edge.com",
"js.driftt.com",
"cloud.feedly.com", # feedly
"static.grammarly.com", # grammarly
"apkpure.com", # apkpure
"download.apkpure.com",
"*.nytimes.com", # New York times
"*.nyt.com",
"bandwagonhost.com", # bandwagonhost
"www.bwh1.net",
"*.akadns.net", # suggestions
"*.haxx.se",
"**.google.com", # google
"**.goo.gl",
"**.googleusercontent.com",
"**.googleapis.com",
"**.youtube.com", # youtube
"**.ytimg.com",
"**.ggpht.com",
"**.googlevideo.com",
"**.facebook.com", # facebook
"**.xx.fbcdn.net",
"**.twitter.com", # twitter
"**.twimg.com",
"**.blogspot.com", # blogspot
"**.wikipedia.org", # wikipeida
]
verbose=1
whitelist=[
"iamp.*.*.*",
"iamp.*.*",
"imap-mail.outlook.com",
"**.qq.com",
"**.baidu.com"
]
verbose=0
+18
View File
@@ -0,0 +1,18 @@
# Compile
FROM golang:1.11-alpine AS compiler
RUN apk add --no-cache git make
# enable go modules
WORKDIR /go-mod
COPY . .
# do not worry about downloading dependency, sower will fix this.
RUN CGO_ENABLED=0 make build
# Build image
FROM scratch
COPY --from=compiler /go-mod/sower /sower
ENTRYPOINT [ "/sower" ]
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>cc.wweir.sower</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/sower</string>
<string>-f</string>
<string>/usr/local/etc/sower.toml</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Executable
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -u -e
[[ -d /tmp/sower ]] || mkdir /tmp/sower
cd /tmp/sower
echo ======== Install sower to your system ========
succ_message(){
echo
echo ======== Installed sower to your system ========
echo The config file is: /usr/local/etc/sower.toml
echo please set 127.0.0.1 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")
printf "Please enter remote server adddress: "
read ADDRESS
echo
curl -SLf https://github.com/wweir/sower/releases/download/$VERSION/sower_darwin_amd64.tar.gz | tar xzv
mv sower /usr/local/bin/
if [[ -r /usr/local/etc/sower.toml ]]; then
echo The config file already exists, keep the original file
else
sed -i~ "s/# server_addr=\"remote-server/server_addr=\"$ADDRESS/" sower.toml
sed -i~ "s/# clear_dns_cache/clear_dns_cache/" sower.toml
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")
printf "Please enter remote server adddress: "
read ADDRESS
echo
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
sed -i~ "s/# server_addr=\"remote-server/server_addr=\"$ADDRESS/" 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
sudo mv sower /usr/local/bin/
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
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Sower client service
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=root
WorkingDirectory=/tmp
ExecStart=/usr/local/bin/sower -f /usr/local/etc/sower.toml
RestartSec=3
Restart=on-failure
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Sower server service
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=nobody
WorkingDirectory=/tmp
ExecStart=/usr/local/bin/sower -n TCP -v 1
RestartSec=3
Restart=on-failure
+20
View File
@@ -0,0 +1,20 @@
#!/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
+74 -61
View File
@@ -7,14 +7,24 @@ import (
"github.com/golang/glog"
"github.com/miekg/dns"
mem "github.com/wweir/mem-go"
"github.com/wweir/sower/conf"
"github.com/wweir/sower/util"
)
func StartDNS(dnsServer string) {
const colon = byte(':')
func StartDNS(dnsServer, listenIP string) {
ip := net.ParseIP(listenIP)
suggest := &intelliSuggest{listenIP, 2 * time.Second, []string{"80", "443"}}
mem.DefaultCache = mem.New(time.Hour)
dnsServer = net.JoinHostPort(dnsServer, "53")
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 {
r.SetTsig(r.Extra[len(r.Extra)-1].(*dns.TSIG).Hdr.Name, dns.HmacMD5, 300, time.Now().Unix())
lastTsig := r.Extra[len(r.Extra)-1].(*dns.TSIG)
r.SetTsig(lastTsig.Hdr.Name, dns.HmacMD5, 300, time.Now().Unix())
}
//https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
@@ -22,83 +32,86 @@ func StartDNS(dnsServer string) {
return
}
if len(conf.Conf.BlockList) == 0 {
bestTry(w, r, r.Question[0].Name, dnsServer)
} else {
manual(w, r, r.Question[0].Name, dnsServer)
domain := r.Question[0].Name
if idx := strings.IndexByte(domain, colon); idx > 0 {
domain = domain[:idx]
}
matchAndServe(w, r, domain, listenIP, dnsServer, ip, suggest)
})
server := &dns.Server{Addr: ":53", Net: "udp"}
server := &dns.Server{Addr: net.JoinHostPort(listenIP, "53"), Net: "udp"}
glog.Fatalln(server.ListenAndServe())
}
func bestTry(w dns.ResponseWriter, r *dns.Msg, domain, dnsServer string) {
msg, _ := dns.Exchange(r, dnsServer+":53")
if msg == nil {
return
}
if len(msg.Answer) == 0 { // expose any response
w.WriteMsg(msg)
return
}
func matchAndServe(w dns.ResponseWriter, r *dns.Msg, domain, listenIP,
dnsServer string, ipNet net.IP, suggest *intelliSuggest) {
var ip string
switch msg.Answer[0].(type) {
case *dns.A:
ip = msg.Answer[0].(*dns.A).A.String()
case *dns.AAAA:
ip = "[" + msg.Answer[0].(*dns.AAAA).AAAA.String() + "]"
default:
w.WriteMsg(msg)
return
}
inWriteList := whiteList.Match(domain)
if _, err := net.DialTimeout("tcp", ip+":http", time.Second); err != nil {
glog.V(2).Infoln(ip+":80", err)
w.WriteMsg(localA(r, domain))
return
}
w.WriteMsg(msg)
}
func manual(w dns.ResponseWriter, r *dns.Msg, domain, dnsServer string) {
if rule.Match(strings.TrimSuffix(domain, ".")) {
if !inWriteList && (blockList.Match(domain) || suggestList.Match(domain)) {
glog.V(2).Infof("match %s suss", domain)
w.WriteMsg(localA(r, domain))
w.WriteMsg(localA(r, domain, ipNet))
return
}
msg, _ := dns.Exchange(r, dnsServer+":53") // expose any response
if msg == nil {
glog.V(1).Infof("get dns of %s fail", domain)
if !inWriteList {
go mem.Remember(suggest, domain)
}
msg, err := dns.Exchange(r, dnsServer)
if msg == nil { // expose any response except nil
glog.V(1).Infof("get dns of %s fail: %s", domain, err)
return
}
w.WriteMsg(msg)
}
if conf.Conf.Verbose != 0 && len(msg.Answer) != 0 {
go func() {
_, err := net.DialTimeout("tcp", domain+":http", 2*time.Second)
if err == nil || !strings.Contains(err.Error(), "timeout") {
return
}
type intelliSuggest struct {
listenIP string
timeout time.Duration
ports []string
}
_, err = net.DialTimeout("tcp", domain+":https", 3*time.Second)
if err == nil || !strings.Contains(err.Error(), "timeout") {
return
}
func (i *intelliSuggest) GetOne(domain interface{}) (iface interface{}, e error) {
iface = struct{}{}
glog.V(1).Infof("SUGGEST check (%s) http(s) service: %s", domain, err)
}()
// kill deadloop, for ugly wildcard setting dns setting
addr := strings.TrimSuffix(domain.(string), ".")
if len(strings.Split(addr, ".")) > 10 {
return
}
}
func localA(r *dns.Msg, domain string) *dns.Msg {
m := new(dns.Msg)
m.SetReply(r)
m.Answer = []dns.RR{&dns.A{
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: conf.Conf.ClientIPNet,
}}
return m
for _, port := range i.ports {
// give local dial a hand, make it not so easy to be added into suggestions
util.HTTPPing(addr+port, addr, i.timeout/5)
localCh := util.HTTPPing(addr+port, addr, i.timeout)
remoteCh := util.HTTPPing(i.listenIP+port, addr, i.timeout)
select {
case err := <-localCh:
if err == nil {
glog.V(2).Infoln(addr, "local first, succ")
continue
}
if err = <-remoteCh; err != nil {
glog.V(2).Infoln(addr, "local first, all fail")
continue
}
glog.V(2).Infoln(addr, "local first, remote succ")
conf.AddSuggest(addr)
case err := <-remoteCh:
if err != nil {
glog.V(2).Infoln(addr, "remote first, fail")
continue
}
glog.V(2).Infoln(addr, "remote first, succ")
conf.AddSuggest(addr)
}
glog.Infof("added suggest domain: %s", addr)
return
}
return
}
-65
View File
@@ -1,65 +0,0 @@
package dns
import (
"strings"
)
type Node struct {
Node map[string]*Node
}
func NewNode() *Node {
return &Node{Node: map[string]*Node{}}
}
func NewNodeFromRule(rules ...string) *Node {
node := NewNode()
for i := range rules {
node.Add(strings.Split(rules[i], "."))
}
return node
}
func (n *Node) String() string {
return n.string("")
}
func (n *Node) string(prefix string) (out string) {
for key, val := range n.Node {
out += prefix + key + "\n" + val.string(prefix+" ")
}
return
}
func (n *Node) Add(secs []string) {
length := len(secs)
switch length {
case 0:
return
case 1:
n.Node[secs[length-1]] = NewNode()
default:
subNode, ok := n.Node[secs[length-1]]
if !ok {
subNode = NewNode()
n.Node[secs[length-1]] = subNode
}
subNode.Add(secs[:length-1])
}
}
func (n *Node) Match(addr string) bool {
return n.matchSecs(strings.Split(addr, "."))
}
func (n *Node) matchSecs(secs []string) bool {
length := len(secs)
if length == 0 {
return len(n.Node) == 0
}
if n, ok := n.Node[secs[length-1]]; ok {
return n.matchSecs(secs[:length-1])
}
_, ok := n.Node["*"]
return ok
}
+43 -5
View File
@@ -1,19 +1,57 @@
package dns
import (
"net"
"github.com/golang/glog"
"github.com/miekg/dns"
"github.com/wweir/sower/conf"
"github.com/wweir/sower/util"
)
var rule *Node
var (
blockList *util.Node
whiteList *util.Node
suggestList *util.Node
)
func init() {
rule = NewNodeFromRule(conf.Conf.BlockList...)
glog.V(2).Infof("block rule:\n%s", rule)
host, _, _ := net.SplitHostPort(conf.Conf.ServerAddr)
//first init
blockList = loadRules("block", conf.Conf.BlockList)
suggestList = loadRules("suggest", conf.Conf.Suggestions)
whiteList = loadRules("white", conf.Conf.WhiteList)
whiteList.Add(host)
conf.OnRefreash = append(conf.OnRefreash, func() error {
rule = NewNodeFromRule(conf.Conf.BlockList...)
glog.V(2).Infof("block rule:\n%s", rule)
blockList = loadRules("block", conf.Conf.BlockList)
suggestList = loadRules("suggest", conf.Conf.Suggestions)
whiteList = loadRules("white", conf.Conf.WhiteList)
whiteList.Add(host)
return nil
})
}
func loadRules(name string, list []string) *util.Node {
rule := util.NewNodeFromRules(".", list...)
glog.V(2).Infof("load %s rule:\n%s", name, rule)
return rule
}
func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg {
m := new(dns.Msg)
m.SetReply(r)
if localIP.To4() != nil {
m.Answer = []dns.RR{&dns.A{
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: localIP,
}}
} else {
m.Answer = []dns.RR{&dns.AAAA{
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20},
AAAA: localIP,
}}
}
return m
}
+15 -10
View File
@@ -1,21 +1,26 @@
module github.com/wweir/sower
require (
github.com/BurntSushi/toml v0.3.1
github.com/bifurcation/mint v0.0.0-20181105071958-a14404e9a861 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/golang/mock v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.0 // indirect
github.com/klauspost/cpuid v1.2.0 // indirect
github.com/klauspost/reedsolomon v1.8.0 // indirect
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f // indirect
github.com/lucas-clemente/quic-go v0.10.0
github.com/lucas-clemente/quic-go v0.10.1
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced // indirect
github.com/miekg/dns v1.1.1
github.com/onsi/ginkgo v1.7.0 // indirect
github.com/onsi/gomega v1.4.3 // indirect
github.com/miekg/dns v1.1.3
github.com/pelletier/go-toml v1.2.0
github.com/pkg/errors v0.8.1
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect
github.com/tjfoc/gmsm v1.0.1 // indirect
github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f // indirect
github.com/wweir/fsnotify v1.4.8
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296
github.com/xtaci/kcp-go v5.0.7+incompatible
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect
)
+30 -40
View File
@@ -1,54 +1,44 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bifurcation/mint v0.0.0-20181105071958-a14404e9a861 h1:x17NvoJaphEzay72TFej4OSSsgu3xRYBLkbIwdofS/4=
github.com/bifurcation/mint v0.0.0-20181105071958-a14404e9a861/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/reedsolomon v1.8.0 h1:lvvOkvk64cE1EGbBIgFk7WSOOsI1GexpuLiT7zjab6g=
github.com/klauspost/reedsolomon v1.8.0/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f h1:sSeNEkJrs+0F9TUau0CgWTTNEwF23HST3Eq0A+QIx+A=
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04=
github.com/lucas-clemente/quic-go v0.10.0 h1:xEF+pSHYAOcu+U10Meunf+DTtc8vhQDRqlA0BJ6hufc=
github.com/lucas-clemente/quic-go v0.10.0/go.mod h1:wuD+2XqEx8G9jtwx5ou2BEYBsE+whgQmlj0Vz/77PrY=
github.com/lucas-clemente/quic-go v0.10.1 h1:ipcMmYP9RT+b1YytOKGUY1qndxPGOczVEQkAVz3CZrs=
github.com/lucas-clemente/quic-go v0.10.1/go.mod h1:wuD+2XqEx8G9jtwx5ou2BEYBsE+whgQmlj0Vz/77PrY=
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced h1:zqEC1GJZFbGZA0tRyNZqRjep92K5fujFtFsu5ZW7Aug=
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=
github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o=
github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM=
github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b h1:mnG1fcsIB1d/3vbkBak2MM0u+vhGhlQwpeimUi7QncM=
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU=
github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
github.com/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/fsnotify v1.4.8 h1:zxOoqwE5p91F3rmcYLQheoSeTlIVqwi2b5T7UlpElWE=
github.com/wweir/fsnotify v1.4.8/go.mod h1:4GuxIc23DXfJI1/LhBXG5eE/wJ4CzL+kk5M7woxVprc=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296 h1:/HkUfg+ZMx/tNdnyJdVlhyv+xO3A7ZlpfL9nFLWLYcc=
github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296/go.mod h1:k7rjBGWoJ+JKwvfe8juAX0zgybjo/Yo3JGkca5f/06s=
github.com/xtaci/kcp-go v5.0.7+incompatible h1:zs9tc8XRID0m+aetu3qPWZFyRt2UIMqbXIBgw+vcnlE=
github.com/xtaci/kcp-go v5.0.7+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b h1:Elez2XeF2p9uyVj0yEUDqQ56NFcDtcBNkYP7yv8YbUE=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+14 -5
View File
@@ -7,13 +7,22 @@ import (
"github.com/wweir/sower/proxy"
)
var version, date string
func main() {
glog.Infoln("Starting:", conf.Conf)
conf := conf.Conf
glog.Infof("Starting sower(%s %s): %v", version, date, conf)
if conf.ServerAddr == "" {
proxy.StartServer(conf.NetType, conf.ServerPort, conf.Cipher, conf.Password)
if conf.Conf.ServerAddr == "" {
proxy.StartServer(conf.Conf.ServerPort)
} else {
go dns.StartDNS(conf.Conf.DnsServer)
proxy.StartClient(conf.Conf.ServerAddr)
if conf.HTTPProxy != "" {
go proxy.StartHttpProxy(conf.NetType, conf.ServerAddr,
conf.Cipher, conf.Password, conf.HTTPProxy)
}
go dns.StartDNS(conf.DNSServer, conf.ClientIP)
proxy.StartClient(conf.NetType, conf.ServerAddr, conf.Cipher, conf.Password, conf.ClientIP)
}
}
+48
View File
@@ -0,0 +1,48 @@
package parse
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/wweir/sower/util"
)
func ParseAddr(conn net.Conn) (teeConn *util.TeeConn, addr string, err error) {
teeConn = &util.TeeConn{Conn: conn}
teeConn.StartOrReset()
defer teeConn.Stop()
buf := make([]byte, 1)
if n, err := teeConn.Read(buf); err != nil {
return teeConn, "", fmt.Errorf("Read conn fail: %v, readed: %v", err, buf[:n])
}
teeConn.StartOrReset()
// https
if buf[0] == 0x16 { // SSL handleshake
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
}
// http
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
}
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
package parse
import (
"encoding/binary"
-75
View File
@@ -1,75 +0,0 @@
package parser
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"strings"
)
type TeeConn struct {
net.Conn
buf []byte
offset int
Tee bool // read
}
func (t *TeeConn) Reset() {
t.offset = 0
}
func (t *TeeConn) Read(b []byte) (n int, err error) {
length := len(t.buf) - t.offset
if length > 0 {
n = copy(b, t.buf[t.offset:])
t.offset += n
return
}
n, err = t.Conn.Read(b)
if t.Tee {
t.buf = append(t.buf, b[:n]...)
t.offset += n
}
return n, err
}
func ParseAddr(conn net.Conn) (teeConn *TeeConn, addr string, err error) {
teeConn = &TeeConn{Conn: conn, Tee: true}
defer func() {
teeConn.Reset()
teeConn.Tee = false
}()
buf := make([]byte, 1)
if n, err := teeConn.Read(buf); err != nil || n != 1 {
return teeConn, "", fmt.Errorf("Read conn fail: %v, readed: %d %v", err, n, buf)
}
teeConn.Reset()
// https
if buf[0] == 0x16 { // SSL handleshake
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
}
// http
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
}
+44 -66
View File
@@ -1,87 +1,64 @@
package proxy
import (
"crypto/tls"
"net"
"time"
"github.com/golang/glog"
"github.com/lucas-clemente/quic-go"
"github.com/wweir/sower/proxy/kcp"
"github.com/wweir/sower/proxy/quic"
"github.com/wweir/sower/proxy/tcp"
"github.com/wweir/sower/shadow"
)
func StartClient(server string) {
connCh := listenLocal([]string{":80", ":443"})
reDialCh := make(chan net.Conn, 10)
var conn net.Conn
type Client interface {
Dial(server string) (net.Conn, error)
}
func NewClient(netType string) Client {
switch netType {
case QUIC.String():
return quic.NewClient()
case KCP.String():
return kcp.NewClient()
case TCP.String():
return tcp.NewClient()
default:
glog.Fatalln("invalid net type: " + netType)
return nil
}
}
func StartClient(netType, server, cipher, password, listenIP string) {
connCh := listenLocal(listenIP, []string{"80", "443"})
client := NewClient(netType)
if addr, err := net.ResolveTCPAddr("tcp", server); err != nil {
glog.Fatalln(err)
} else {
server = addr.String()
}
glog.Infoln("Client started.")
for {
sess, err := quic.DialAddr(server, &tls.Config{InsecureSkipVerify: true}, nil)
conn := <-connCh
glog.V(1).Infof("new conn from (%s) to (%s)", conn.RemoteAddr(), server)
rc, err := client.Dial(server)
if err != nil {
if sess, err = quic.DialAddr(server, &tls.Config{InsecureSkipVerify: true}, nil); err != nil {
glog.Errorf("connect to remote(%s) fail:%s\n", server, err)
time.Sleep(2 * time.Second)
continue
}
conn.Close()
glog.Errorln(err)
continue
}
glog.Infof("new session from (%s) to (%s)", sess.LocalAddr(), sess.RemoteAddr())
rc = shadow.Shadow(rc, cipher, password)
for { // session rotate logic
select {
case conn = <-connCh:
case conn = <-reDialCh:
}
// sync action to reuse sigle sess
if !openStream(conn, sess, reDialCh) {
sess.Close()
break
}
}
go relay(conn, rc)
}
}
func openStream(conn net.Conn, sess quic.Session, reDialCh chan<- net.Conn) bool {
glog.V(2).Infoln("new request from", conn.RemoteAddr())
okCh := make(chan struct{})
go func() {
stream, err := sess.OpenStream()
if err != nil {
glog.Warningf("start stream to (%s) fail:%s\n", sess.RemoteAddr(), err)
reDialCh <- conn
close(okCh)
return
}
defer stream.Close()
select {
case okCh <- struct{}{}:
default:
close(okCh)
return
}
close(okCh)
if err := conn.(*net.TCPConn).SetKeepAlive(true); err != nil {
glog.Warningln(err)
}
relay(&streamConn{stream, sess}, conn)
conn.Close()
}()
select {
case _, ok := <-okCh: // false means close on error
return ok
case <-time.After(500 * time.Millisecond):
return false
}
}
func listenLocal(ports []string) <-chan net.Conn {
func listenLocal(listenIP string, ports []string) <-chan net.Conn {
connCh := make(chan net.Conn, 10)
for i := range ports {
go func(port string) {
ln, err := net.Listen("tcp", port)
ln, err := net.Listen("tcp", net.JoinHostPort(listenIP, port))
if err != nil {
glog.Fatalln(err)
}
@@ -89,10 +66,11 @@ func listenLocal(ports []string) <-chan net.Conn {
for {
conn, err := ln.Accept()
if err != nil {
glog.Errorln("accept", port, "fail:", err)
glog.Errorln("accept", listenIP+port, "fail:", err)
continue
}
conn.(*net.TCPConn).SetKeepAlive(true)
connCh <- conn
}
}(ports[i])
+98
View File
@@ -0,0 +1,98 @@
package proxy
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"time"
"github.com/golang/glog"
"github.com/wweir/sower/shadow"
)
func StartHttpProxy(netType, server, cipher, password, addr string) {
client := NewClient(netType)
if addr, err := net.ResolveTCPAddr("tcp", server); err != nil {
glog.Fatalln(err)
} else {
server = addr.String()
}
srv := &http.Server{
Addr: addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
httpsProxy(w, r, client, server, cipher, password)
} else {
httpProxy(w, r, client, server, cipher, password)
}
}),
// Disable HTTP/2.
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
IdleTimeout: 90 * time.Second,
}
glog.Fatalln(srv.ListenAndServe())
}
func httpProxy(w http.ResponseWriter, req *http.Request, client Client, server, cipher, password string) {
roundTripper := &http.Transport{
DialContext: func(context.Context, string, string) (net.Conn, error) {
conn, err := client.Dial(server)
if err != nil {
return nil, err
}
return shadow.Shadow(conn, cipher, password), nil
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp, err := roundTripper.RoundTrip(req)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
glog.Errorln("serve https proxy, get remote data:", err)
return
}
defer resp.Body.Close()
for k, vs := range resp.Header {
for _, v := range vs {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func httpsProxy(w http.ResponseWriter, r *http.Request, client Client, server, cipher, password string) {
// local conn
conn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
if _, err := conn.Write([]byte(r.Proto + " 200 Connection established\r\n\r\n")); err != nil {
conn.Close()
glog.Errorln("serve https proxy, write data fail:", err)
return
}
// remote conn
rc, err := client.Dial(server)
if err != nil {
conn.Close()
http.Error(w, err.Error(), http.StatusServiceUnavailable)
glog.Errorln("serve https proxy, dial remote fail:", err)
return
}
rc = shadow.Shadow(rc, cipher, password)
relay(rc, conn)
}
+65
View File
@@ -0,0 +1,65 @@
package kcp
import (
"net"
"github.com/pkg/errors"
kcp "github.com/xtaci/kcp-go"
)
type client struct {
DataShard int
ParityShard int
DSCP int
SockBuf int
AckNodelay bool
NoDelay int
Interval int
Resend int
NoCongestion int
SndWnd int
RcvWnd int
MTU int
}
func NewClient() *client {
return &client{
DataShard: 10,
ParityShard: 3,
DSCP: 0,
SockBuf: 4194304,
NoDelay: 0,
Interval: 50,
Resend: 0,
NoCongestion: 0,
SndWnd: 0,
RcvWnd: 0,
MTU: 1350,
}
}
func (c *client) Dial(server string) (net.Conn, error) {
conn, err := kcp.DialWithOptions(server, nil, c.DataShard, c.ParityShard)
if err != nil {
return nil, errors.Wrap(err, "dial")
}
conn.SetStreamMode(true)
conn.SetWriteDelay(false)
conn.SetNoDelay(c.NoDelay, c.Interval, c.Resend, c.NoCongestion)
conn.SetWindowSize(c.SndWnd, c.RcvWnd)
conn.SetMtu(c.MTU)
conn.SetACKNoDelay(c.AckNodelay)
if err := conn.SetDSCP(c.DSCP); err != nil {
return nil, errors.Wrap(err, "SetDSCP")
}
if err := conn.SetReadBuffer(c.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetReadBuffer")
}
if err := conn.SetWriteBuffer(c.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetWriteBuffer")
}
return conn, nil
}
+56
View File
@@ -0,0 +1,56 @@
package kcp
import (
"net"
"github.com/golang/glog"
"github.com/pkg/errors"
kcp "github.com/xtaci/kcp-go"
)
type server struct {
DataShard int
ParityShard int
DSCP int
SockBuf int
}
func NewServer() *server {
return &server{
DataShard: 10,
ParityShard: 3,
DSCP: 0,
SockBuf: 4194304,
}
}
func (s *server) Listen(port string) (<-chan net.Conn, error) {
ln, err := kcp.ListenWithOptions(port, nil, s.DataShard, s.ParityShard)
if err != nil {
return nil, err
}
if err := ln.SetDSCP(s.DSCP); err != nil {
return nil, errors.Wrap(err, "SetDSCP")
}
if err := ln.SetReadBuffer(s.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetReadBuffer")
}
if err := ln.SetWriteBuffer(s.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetWriteBuffer")
}
connCh := make(chan net.Conn)
go func() {
for {
conn, err := ln.AcceptKCP()
if err != nil {
glog.Fatalln("KCP listen:", err)
}
connCh <- conn
}
}()
return connCh, nil
}
+16
View File
@@ -0,0 +1,16 @@
// Code generated by "stringer -type=netType util.go"; DO NOT EDIT.
package proxy
import "strconv"
const _netType_name = "QUICKCPTCP"
var _netType_index = [...]uint8{0, 4, 7, 10}
func (i netType) String() string {
if i < 0 || i >= netType(len(_netType_index)-1) {
return "netType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _netType_name[_netType_index[i]:_netType_index[i+1]]
}
+57
View File
@@ -0,0 +1,57 @@
package quic
import (
"crypto/tls"
"net"
"time"
quic "github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
"github.com/wweir/sower/util"
)
type client struct {
conf *quic.Config
sess quic.Session
}
func NewClient() *client {
return &client{
conf: &quic.Config{
HandshakeTimeout: 5 * time.Second,
MaxIncomingStreams: 1024,
KeepAlive: true,
IdleTimeout: 5 * time.Minute,
},
}
}
func (c *client) Dial(server string) (net.Conn, error) {
if c.sess == nil {
if sess, err := quic.DialAddr(server, &tls.Config{InsecureSkipVerify: true}, c.conf); err != nil {
return nil, errors.Wrap(err, "session")
} else {
go func() {
<-sess.Context().Done()
sess.Close()
c.sess = nil
}()
c.sess = sess
}
}
var stream quic.Stream
if err := util.WithTimeout(func() (err error) {
if stream, err = c.sess.OpenStream(); err != nil {
c.sess = nil
}
return
}, time.Second); err != nil {
return nil, errors.Wrap(err, "stream")
}
return &streamConn{
Stream: stream,
sess: c.sess,
}, nil
}
+59
View File
@@ -0,0 +1,59 @@
package quic
import (
"net"
"time"
"github.com/golang/glog"
quic "github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
)
type server struct {
conf *quic.Config
}
func NewServer() *server {
return &server{
conf: &quic.Config{
HandshakeTimeout: 5 * time.Second,
MaxIncomingStreams: 1024,
KeepAlive: true,
IdleTimeout: 5 * time.Minute,
},
}
}
func (s *server) Listen(port string) (<-chan net.Conn, error) {
ln, err := quic.ListenAddr(port, mockTlsPem(), s.conf)
if err != nil {
return nil, errors.WithStack(err)
}
connCh := make(chan net.Conn)
go func() {
for {
sess, err := ln.Accept()
if err != nil {
glog.Fatalln(err)
}
go accept(sess, connCh)
}
}()
return connCh, nil
}
func accept(sess quic.Session, connCh chan<- net.Conn) {
glog.V(1).Infoln("new session from ", sess.RemoteAddr())
defer sess.Close()
for {
stream, err := sess.AcceptStream()
if err != nil {
glog.Errorln(err)
return
}
connCh <- &streamConn{stream, sess}
}
}
+48
View File
@@ -0,0 +1,48 @@
package quic
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"math/big"
"net"
"github.com/golang/glog"
quic "github.com/lucas-clemente/quic-go"
)
type streamConn struct {
quic.Stream
sess quic.Session
}
func (s *streamConn) LocalAddr() net.Addr {
return s.sess.LocalAddr()
}
func (s *streamConn) RemoteAddr() net.Addr {
return s.sess.RemoteAddr()
}
func mockTlsPem() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
glog.Fatalln(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
glog.Fatalln(err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
glog.Fatalln(err)
}
return &tls.Config{Certificates: []tls.Certificate{tlsCert}}
}
+43 -33
View File
@@ -2,62 +2,72 @@ package proxy
import (
"net"
"strings"
"github.com/golang/glog"
"github.com/lucas-clemente/quic-go"
"github.com/wweir/sower/parser"
"github.com/wweir/sower/parse"
"github.com/wweir/sower/proxy/kcp"
"github.com/wweir/sower/proxy/quic"
"github.com/wweir/sower/proxy/tcp"
"github.com/wweir/sower/shadow"
)
func StartServer(port string) {
ln, err := quic.ListenAddr(":"+port, mockTlsPem(), nil)
if err != nil {
glog.Fatalln(err)
}
type Server interface {
Listen(port string) (<-chan net.Conn, error)
}
for {
sess, err := ln.Accept()
if err != nil {
glog.Errorln(err)
continue
}
go acceptSession(sess)
func NewServer(netType string) Server {
switch netType {
case QUIC.String():
return quic.NewServer()
case KCP.String():
return kcp.NewServer()
case TCP.String():
return tcp.NewServer()
default:
glog.Fatalln("invalid net type: " + netType)
return nil
}
}
func acceptSession(sess quic.Session) {
glog.V(1).Infoln("new session from ", sess.RemoteAddr())
defer sess.Close()
func StartServer(netType, port, cipher, password string) {
if port == "" {
glog.Fatalln("port must set")
}
if !strings.Contains(port, ":") {
port = ":" + port
}
connCh, err := NewServer(netType).Listen(port)
if err != nil {
glog.Fatalf("listen %v fail: %s", port, err)
}
glog.Infoln("Server started.")
for {
stream, err := sess.AcceptStream()
if err != nil {
glog.Errorln(err)
return
}
conn := <-connCh
conn = shadow.Shadow(conn, cipher, password)
go acceptStream(stream, sess)
go handle(conn)
}
}
func acceptStream(stream quic.Stream, sess quic.Session) {
glog.V(1).Infoln("new stream from ", sess.RemoteAddr())
defer stream.Close()
conn, addr, err := parser.ParseAddr(&streamConn{stream, sess})
func handle(conn net.Conn) {
conn, addr, err := parse.ParseAddr(conn)
if err != nil {
conn.Close()
glog.Warningln(err)
return
}
glog.V(1).Infoln(addr)
glog.V(1).Infof("new conn from %s to %s", conn.RemoteAddr(), addr)
rc, err := net.Dial("tcp", addr)
if err != nil {
conn.Close()
glog.Warningln(err)
return
}
defer rc.Close()
if err := rc.(*net.TCPConn).SetKeepAlive(true); err != nil {
glog.Warningln(err)
}
rc.(*net.TCPConn).SetKeepAlive(true)
relay(rc, conn)
}
+26
View File
@@ -0,0 +1,26 @@
package tcp
import (
"net"
"time"
)
type client struct {
DialTimeout time.Duration
}
func NewClient() *client {
return &client{
DialTimeout: 5 * time.Second,
}
}
func (c *client) Dial(server string) (net.Conn, error) {
conn, err := net.DialTimeout("tcp", server, c.DialTimeout)
if err != nil {
return nil, err
}
conn.(*net.TCPConn).SetKeepAlive(true)
return conn, nil
}
+35
View File
@@ -0,0 +1,35 @@
package tcp
import (
"net"
"github.com/golang/glog"
)
type server struct {
}
func NewServer() *server {
return &server{}
}
func (s *server) Listen(port string) (<-chan net.Conn, error) {
ln, err := net.Listen("tcp", port)
if err != nil {
return nil, err
}
connCh := make(chan net.Conn)
go func() {
for {
conn, err := ln.Accept()
if err != nil {
glog.Fatalln("TCP listen:", err)
}
conn.(*net.TCPConn).SetKeepAlive(true)
connCh <- conn
}
}()
return connCh, nil
}
+22 -49
View File
@@ -1,74 +1,47 @@
package proxy
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"io"
"math/big"
"net"
"sync"
"sync/atomic"
"time"
"github.com/golang/glog"
"github.com/lucas-clemente/quic-go"
)
type streamConn struct {
quic.Stream
sess quic.Session
}
//go:generate stringer -type=netType $GOFILE
type netType int
func (s *streamConn) LocalAddr() net.Addr {
return s.sess.LocalAddr()
}
func (s *streamConn) RemoteAddr() net.Addr {
return s.sess.RemoteAddr()
}
const (
QUIC netType = iota
KCP
TCP
)
func relay(conn1, conn2 net.Conn) {
wg := &sync.WaitGroup{}
exitFlag := new(int32)
wg.Add(2)
go redirect(conn1, conn2, wg, exitFlag)
redirect(conn2, conn1, wg, exitFlag)
go redirect(conn2, conn1, wg, exitFlag)
redirect(conn1, conn2, wg, exitFlag)
wg.Wait()
}
func redirect(conn1, conn2 net.Conn, wg *sync.WaitGroup, exitFlag *int32) {
if _, err := io.Copy(conn2, conn1); err != nil && (atomic.LoadInt32(exitFlag) == 0) {
glog.V(1).Infof("%s<>%s -> %s<>%s: %s", conn1.RemoteAddr(), conn1.LocalAddr(), conn2.LocalAddr(), conn2.RemoteAddr(), err)
func redirect(dst, src net.Conn, wg *sync.WaitGroup, exitFlag *int32) {
if _, err := io.Copy(dst, src); err != nil {
glog.V(1).Infof("%s<>%s -> %s<>%s: %s", src.RemoteAddr(), src.LocalAddr(), dst.LocalAddr(), dst.RemoteAddr(), err)
}
if atomic.CompareAndSwapInt32(exitFlag, 0, 1) {
// wakeup blocked goroutine
now := time.Now()
src.SetDeadline(now)
dst.SetDeadline(now)
} else {
src.Close()
dst.Close()
}
// wakeup all conn goroutine
atomic.AddInt32(exitFlag, 1)
now := time.Now()
conn1.SetDeadline(now)
conn2.SetDeadline(now)
wg.Done()
}
func mockTlsPem() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
glog.Fatalln(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
glog.Fatalln(err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
glog.Fatalln(err)
}
return &tls.Config{Certificates: []tls.Certificate{tlsCert}}
}
+67
View File
@@ -0,0 +1,67 @@
package shadow
import (
"crypto/aes"
"crypto/cipher"
"github.com/pkg/errors"
"golang.org/x/crypto/chacha20poly1305"
)
//go:generate stringer -type=cipherType $GOFILE
type cipherType int
const (
AES_128_GCM cipherType = iota
AES_192_GCM
AES_256_GCM
CHACHA20_IETF_POLY1305
XCHACHA20_IETF_POLY1305
)
func pickCipher(cipherType, password string) (cipher.AEAD, error) {
var blockSize int
switch cipherType {
case AES_128_GCM.String():
blockSize = 16
case AES_192_GCM.String():
blockSize = 24
case AES_256_GCM.String():
blockSize = 32
case CHACHA20_IETF_POLY1305.String():
return chacha20poly1305.New(genKey(password, 256))
case XCHACHA20_IETF_POLY1305.String():
return chacha20poly1305.NewX(genKey(password, 256))
default:
return nil, errors.New("do not support cipher type: " + cipherType)
}
// aes gcm
block, err := aes.NewCipher(genKey(password, blockSize))
if err != nil {
return nil, errors.Wrap(err, "password")
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(err, AES_128_GCM.String())
}
return aead, nil
}
func genKey(filler string, size int) []byte {
res := make([]byte, size)
if filler == "" {
panic("password should not be empty")
}
fillerByte := []byte(filler)
length := len(fillerByte)
for i := 0; ; i++ {
if copy(res[i*length:], fillerByte) != length {
return res
}
}
}
+16
View File
@@ -0,0 +1,16 @@
// Code generated by "stringer -type=cipherType cipher.go"; DO NOT EDIT.
package shadow
import "strconv"
const _cipherType_name = "AES_128_GCMAES_192_GCMAES_256_GCMCHACHA20_IETF_POLY1305XCHACHA20_IETF_POLY1305"
var _cipherType_index = [...]uint8{0, 11, 22, 33, 55, 78}
func (i cipherType) String() string {
if i < 0 || i >= cipherType(len(_cipherType_index)-1) {
return "cipherType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _cipherType_name[_cipherType_index[i]:_cipherType_index[i+1]]
}
+4
View File
@@ -0,0 +1,4 @@
// Package shadow transter conn to be a crypto conn
// support aead mode only
// payload: [size](2+Overhead bytes) + [content](size+Overhead bytes)
package shadow
+111
View File
@@ -0,0 +1,111 @@
package shadow
import (
"crypto/cipher"
"encoding/binary"
"io"
"math/rand"
"net"
)
const MAX_SIZE = 0xFFFF
type conn struct {
maxSize int
aead cipher.AEAD
encryptNonce func() []byte
decryptNonce func() []byte
writeBuf []byte
readBuf []byte
readOffset int
net.Conn
}
func (c *conn) Read(b []byte) (n int, err error) {
// read from buffer
if c.readOffset != 0 {
dataSize := len(c.readBuf) - c.aead.Overhead()
n = copy(b, c.readBuf[c.readOffset:dataSize])
c.readOffset += n
if c.readOffset == dataSize {
c.readOffset = 0
}
return
}
// read from conn
dataSize := 0
{ //read data size
c.readBuf = make([]byte, 2+c.aead.Overhead())
if _, err = io.ReadFull(c.Conn, c.readBuf); err != nil {
return
}
if _, err = c.aead.Open(c.readBuf[:0], c.decryptNonce(), c.readBuf, nil); err != nil {
return
}
dataSize = int(c.readBuf[0])<<8 + int(c.readBuf[1])
}
{ // read data
c.readBuf = make([]byte, dataSize+c.aead.Overhead())
if _, err = io.ReadFull(c.Conn, c.readBuf); err != nil {
return
}
if _, err = c.aead.Open(c.readBuf[:0], c.decryptNonce(), c.readBuf, nil); err != nil {
return
}
}
// buffer extra data
if n = copy(b, c.readBuf[:dataSize]); n < dataSize {
c.readOffset = n
}
return
}
func (c *conn) Write(b []byte) (n int, err error) {
bLen := len(b)
dataSize := MAX_SIZE - (2 + c.aead.Overhead()) - c.aead.Overhead()
if bLen < c.maxSize {
dataSize = bLen
}
// BigEndian
c.writeBuf[0], c.writeBuf[1] = byte((dataSize&0xFF00)>>8), byte(dataSize&0xFF)
c.aead.Seal(c.writeBuf[:0], c.encryptNonce(), c.writeBuf[:2], nil)
c.aead.Seal(c.writeBuf[:2+c.aead.Overhead()], c.encryptNonce(), b[:dataSize], nil)
_, err = c.Conn.Write(c.writeBuf[:dataSize+(2+c.aead.Overhead())+c.aead.Overhead()])
if err != nil {
return 0, err
}
return dataSize, err
}
func Shadow(c net.Conn, cipher, password string) net.Conn {
aead, err := pickCipher(cipher, password)
if err != nil {
panic(err)
}
return &conn{
maxSize: MAX_SIZE - (2 - aead.Overhead()) - aead.Overhead(),
aead: aead,
encryptNonce: newNonce(password, aead.NonceSize()),
decryptNonce: newNonce(password, aead.NonceSize()),
writeBuf: make([]byte, 0xFFFF),
Conn: c,
}
}
func newNonce(password string, size int) func() []byte {
num, _ := binary.Varint([]byte(password))
rnd := rand.New(rand.NewSource(num))
buf := make([]byte, size)
return func() []byte {
rnd.Read(buf)
return buf
}
}
+121
View File
@@ -0,0 +1,121 @@
package util
import (
"bytes"
"crypto/tls"
"encoding/binary"
"io"
"net"
"time"
)
// HTTPPing try connect to a http(s) server with domain though the http addr
func HTTPPing(tcpAddr, domain string, timeout time.Duration) <-chan error {
errCh := make(chan error)
go func() {
errCh <- httpPing(tcpAddr, domain, timeout)
}()
return errCh
}
func httpPing(tcpAddr, domain string, timeout time.Duration) error {
conn, err := net.DialTimeout("tcp", tcpAddr, timeout)
if err != nil {
return err
}
defer conn.Close()
host, port, err := net.SplitHostPort(tcpAddr)
if err != nil {
return err
}
var msg []byte
switch port {
case ":80":
msg = []byte("TRACE / HTTP/1.1\r\nHost: " + domain + "\r\n\r\n")
case ":443":
msg = NewClientHelloSNIMsg(domain)
}
conn.SetDeadline(time.Now().Add(timeout))
if _, err = conn.Write(msg); err != nil {
return err
}
// err -> nil: read something succ
// err -> io.EOF: no such domain or connection refused
// err -> timeout: tcp package has been dropped
_, err = conn.Read(make([]byte, 1))
if err == io.EOF && host == domain {
return nil
}
return err
}
type clientHelloSNI struct {
ContentType uint8
Version uint16
Length uint16
handshakeProtocol
}
type handshakeProtocol struct {
HandshakeType uint8
LengthExpand uint8
Length uint16
Version uint16
Random [32]byte
SessionIDLength uint8
CipherSuitesLength uint16
CipherSuite uint16
CompressionMethodsLength uint8
CompressionMethod uint8
ExtensionsLength uint16
extensionServerName
}
type extensionServerName struct {
Type uint16
Length uint16
serverNameIndicationExtension
}
type serverNameIndicationExtension struct {
ServerNameListlength uint16
ServerNameType uint8
ServerNamelength uint16
// ServerName []byte // Disable for fix length
}
func NewClientHelloSNIMsg(domain string) []byte {
length := uint16(len(domain))
msg := &clientHelloSNI{
ContentType: 0x16, // Content Type: Handshake (22)
Version: 0x0301, // Version: TLS 1.0 (0x0301)
Length: length + 56,
handshakeProtocol: handshakeProtocol{
HandshakeType: 0x01, // Handshake Type: Client Hello (1)
Length: length + 52,
Version: 0x0303, // Version: TLS 1.2 (0x0303)
Random: [32]byte{}, // [32]byte{},
SessionIDLength: 0x0, // Session ID Length: 0
CipherSuitesLength: 2, // Cipher Suites Length: 84
CipherSuite: tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
CompressionMethodsLength: 1, // Compression Methods Length: 1
CompressionMethod: 0x00, // Compression null
ExtensionsLength: length + 9,
extensionServerName: extensionServerName{
Type: 0x0000, // Type: server_name (0)
Length: length + 5,
serverNameIndicationExtension: serverNameIndicationExtension{
ServerNameListlength: length + 3,
ServerNameType: 0x00, // Server Name Type: host_name (0)
ServerNamelength: length,
},
},
},
}
buf := bytes.NewBuffer(make([]byte, 0, length+61))
binary.Write(buf, binary.BigEndian, msg)
buf.WriteString(domain)
return buf.Bytes()
}
+52
View File
@@ -0,0 +1,52 @@
package util
import (
"sort"
"strings"
)
type ReverseSecSlice struct {
sort.StringSlice
}
func NewReverseSecSlice(a []string) *ReverseSecSlice {
return &ReverseSecSlice{sort.StringSlice(a)}
}
func (p *ReverseSecSlice) Sort() *ReverseSecSlice {
sort.Sort(p)
return p
}
func (p *ReverseSecSlice) Uniq() []string {
olds := []string(p.StringSlice)
last := ""
strs := make([]string, 0, len(olds))
for _, str := range olds {
if str != last {
strs = append(strs, str)
}
last = str
}
return strs
}
func (p *ReverseSecSlice) Less(i, j int) bool {
secsI := strings.Split(p.StringSlice[i], ".")
secsJ := strings.Split(p.StringSlice[j], ".")
lenI := len(secsI) - 1
lenJ := len(secsJ) - 1
length := lenI
if lenI > lenJ {
length = lenJ
}
for idx := 0; idx <= length; idx++ {
if secsI[lenI-idx] == secsJ[lenJ-idx] {
continue
}
return secsI[lenI-idx] < secsJ[lenJ-idx]
}
return lenI < lenJ
}
+65
View File
@@ -0,0 +1,65 @@
package util
import (
"sort"
"testing"
)
func TestReverseSecSlice_Less(t *testing.T) {
type fields struct {
StringSlice sort.StringSlice
}
type args struct {
i int
j int
}
slice := fields{sort.StringSlice([]string{
"a.b.c",
"a.b.c",
"d.b.c",
"d.a.b.c",
"",
"",
})}
tests := []struct {
name string
fields fields
args args
want bool
}{{
name: "equal",
fields: slice,
args: args{0, 1},
want: false,
}, {
name: "less",
fields: slice,
args: args{0, 2},
want: true,
}, {
name: "length",
fields: slice,
args: args{0, 3},
want: true,
}, {
name: "single_empty",
fields: slice,
args: args{0, 4},
want: false,
}, {
name: "all_empty",
fields: slice,
args: args{4, 5},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &ReverseSecSlice{
StringSlice: tt.fields.StringSlice,
}
if got := p.Less(tt.args.i, tt.args.j); got != tt.want {
t.Errorf("ReverseSecSlice.Less() = %v, want %v", got, tt.want)
}
})
}
}
+101
View File
@@ -0,0 +1,101 @@
package util
import (
"strings"
"sync"
)
type Node struct {
node
sep string
*sync.RWMutex
}
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)
for i := range rules {
n.Add(rules[i])
}
return n
}
func (n *Node) String() string {
n.RLock()
defer n.RUnlock()
return n.string("", " ")
}
func (n *node) string(prefix, indent string) (out string) {
for key, val := range n.node {
out += prefix + key + "\n" + val.string(prefix+indent, indent)
}
return
}
func (n *Node) trim(item string) string {
return strings.TrimSuffix(item, n.sep)
}
func (n *Node) Add(item string) {
n.Lock()
defer n.Unlock()
n.add(strings.Split(n.trim(item), n.sep))
}
func (n *node) add(secs []string) {
length := len(secs)
switch length {
case 0:
return
case 1:
n.node[secs[length-1]] = &node{node: map[string]*node{"": &node{}}}
default:
sec := secs[length-1]
if sec == "**" {
sec = "*"
}
subNode, ok := n.node[sec]
if !ok {
subNode = &node{node: map[string]*node{}}
n.node[sec] = subNode
}
subNode.add(secs[:length-1])
}
}
func (n *Node) Match(item string) bool {
return n.matchSecs(strings.Split(n.trim(item), n.sep), false)
}
func (n *node) matchSecs(secs []string, fuzzNode bool) bool {
length := len(secs)
if length == 0 {
if _, ok := n.node[""]; ok {
return true
}
if _, ok := n.node["**"]; ok {
return true
}
if _, ok := n.node["*"]; ok {
return !fuzzNode
}
return false
}
if n, ok := n.node[secs[length-1]]; ok {
return n.matchSecs(secs[:length-1], false)
}
if n, ok := n.node["*"]; ok {
return n.matchSecs(secs[:length-1], true)
}
if _, ok := n.node["**"]; ok {
return true
}
return false
}
+77
View File
@@ -0,0 +1,77 @@
package util
import (
"testing"
)
func TestNode_Match(t *testing.T) {
type test struct {
arg string
want bool
}
tests := []struct {
name string
node *Node
tests []test
}{{
"simple",
NewNodeFromRules(".", "a.wweir.cc", "b.wweir.cc"),
[]test{
{"a.wweir.cc", true},
{"b.wweir.cc", true},
},
}, {
"parent",
NewNodeFromRules(".", "wweir.cc", "a.wweir.cc"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"b.wweir.cc", false},
},
}, {
"fuzz1",
NewNodeFromRules(".", "wweir.cc", "a.wweir.cc", "*.wweir.cc"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"b.wweir.cc", true},
{"a.b.wweir.cc", false},
},
}, {
"fuzz2",
NewNodeFromRules(".", "a.*.cc"),
[]test{
{"wweir.cc", false},
{"a.wweir.cc", true},
{"b.wweir.cc", false},
},
}, {
"fuzz3",
NewNodeFromRules(".", "*.*.cc", "iamp.*.*"),
[]test{
{"wweir.cc", false},
{"a.wweir.cc", true},
{"b.wweir.cc", true},
{"iamp.wweir.cc", true},
},
}, {
"fuzz4",
NewNodeFromRules(".", "**.cc", "a.**.com"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"a.b.wweir.cc", true},
{"a.wweir.com", true},
{"b.wweir.com", false},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, test := range tt.tests {
if got := tt.node.Match(test.arg); got != test.want {
t.Errorf("Node.Match(%s) = %v, want %v", test.arg, got, test.want)
}
}
})
}
}
+37
View File
@@ -0,0 +1,37 @@
package util
import (
"net"
)
type TeeConn struct {
net.Conn
buf []byte
offset int
tee bool // read
}
func (t *TeeConn) StartOrReset() {
t.offset = 0
t.tee = true
}
func (t *TeeConn) Stop() {
t.offset = 0
t.tee = false
}
func (t *TeeConn) Read(b []byte) (n int, err error) {
length := len(t.buf) - t.offset
if length > 0 {
n = copy(b, t.buf[t.offset:])
t.offset += n
return
}
n, err = t.Conn.Read(b)
if t.tee {
t.buf = append(t.buf, b[:n]...)
t.offset += n
}
return n, err
}
+23
View File
@@ -0,0 +1,23 @@
package util
import (
"errors"
"time"
)
func WithTimeout(fn func() error, timeout time.Duration) error {
var okCh = make(chan struct{})
var err error
go func() {
err = fn()
close(okCh)
}()
select {
case <-okCh:
return err
case <-time.After(timeout):
return errors.New("timeout: " + timeout.String())
}
}