mirror of
https://github.com/wweir/sower.git
synced 2024-04-21 12:42:15 +00:00
refactor, start v0.8
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
# Compile
|
||||
FROM golang:1.14-alpine AS compiler
|
||||
|
||||
RUN apk add --no-cache git make
|
||||
|
||||
# enable go modules
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 make build
|
||||
|
||||
|
||||
# Build image
|
||||
FROM scratch
|
||||
|
||||
COPY --from=compiler /src/sower /sower
|
||||
ENTRYPOINT [ "/sower" ]
|
||||
+5
-3
@@ -17,6 +17,8 @@
|
||||
*.swo
|
||||
.vscode
|
||||
.idea
|
||||
sower
|
||||
sower.exe*
|
||||
/sower.toml
|
||||
|
||||
/cmd/client/client
|
||||
/client
|
||||
/cmd/server/server
|
||||
/server
|
||||
@@ -1,12 +1,22 @@
|
||||
CPUS ?= $(shell nproc)
|
||||
MAKEFLAGS += --jobs=$(CPUS)
|
||||
GO:=CGO_ENABLED=0 go
|
||||
|
||||
default: test build
|
||||
|
||||
test:
|
||||
go vet ./...
|
||||
go list ./... | grep -v internal | xargs go test
|
||||
build:
|
||||
go build -ldflags "-w -s \
|
||||
-X conf.version=$(shell git describe --tags --always) \
|
||||
-X conf.date=$(shell date +%Y-%m-%d)"
|
||||
image:
|
||||
docker build -t sower -f .github/Dockerfile .
|
||||
${GO} vet ./...
|
||||
${GO} test ./...
|
||||
|
||||
build: client server
|
||||
|
||||
.PHONY: client
|
||||
client:
|
||||
${GO} build -ldflags "\
|
||||
-X main.version=$(shell git describe --tags --always) \
|
||||
-X main.date=$(shell date +%Y-%m-%d)" ./cmd/client
|
||||
.PHONY: server
|
||||
server:
|
||||
${GO} build -ldflags "\
|
||||
-X main.version=$(shell git describe --tags --always) \
|
||||
-X main.date=$(shell date +%Y-%m-%d)" ./cmd/server
|
||||
@@ -0,0 +1,219 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cristalhq/aconfig"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
"github.com/wweir/sower/pkg/teeconn"
|
||||
"github.com/wweir/sower/router"
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/sower/transport/sower"
|
||||
"github.com/wweir/sower/transport/trojan"
|
||||
"github.com/wweir/sower/util"
|
||||
)
|
||||
|
||||
var (
|
||||
version, date string
|
||||
|
||||
conf = struct {
|
||||
Proxy struct {
|
||||
Type string `default:"sower" usage:"remote proxy protocol, sower/trojan"`
|
||||
Addr string `required:"true" usage:"remote proxy address, eg: proxy.com"`
|
||||
Password string `required:"true" usage:"remote proxy password"`
|
||||
}
|
||||
|
||||
Socks5Addr string `default:":1080" usage:"socks5 listen address"`
|
||||
FallbackDNS string `default:"223.5.5.5" usage:"fallback dns server"`
|
||||
DNSServeIP string `usage:"dns server ip, eg: 127.0.0.1"`
|
||||
|
||||
Router struct {
|
||||
ProxyList []string
|
||||
ProxyRefs []string
|
||||
DirectList []string
|
||||
DirectRefs []string
|
||||
BlockList []string
|
||||
BlockRefs []string
|
||||
}
|
||||
}{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.ErrorStackMarshaler = func(err error) interface{} {
|
||||
return pkgerrors.MarshalStack(err)
|
||||
}
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: time.StampMilli,
|
||||
FormatCaller: func(i interface{}) string {
|
||||
caller := i.(string)
|
||||
if idx := strings.Index(caller, "/pkg/mod/"); idx > 0 {
|
||||
return caller[idx+9:]
|
||||
}
|
||||
if idx := strings.LastIndexByte(caller, '/'); idx > 0 {
|
||||
return caller[idx+1:]
|
||||
}
|
||||
return caller
|
||||
},
|
||||
}).With().Timestamp().Caller().Logger()
|
||||
|
||||
if err := aconfig.LoaderFor(&conf, aconfig.Config{}).Load(); err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Msg("Load config")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("version", version).
|
||||
Str("date", date).
|
||||
Interface("config", conf).
|
||||
Msg("Starting")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
r := router.NewRouter(genProxyDial())
|
||||
|
||||
go func() {
|
||||
lnHTTP, err := net.Listen("tcp", net.JoinHostPort(conf.DNSServeIP, "80"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen port")
|
||||
}
|
||||
go ServeHTTP(lnHTTP, r)
|
||||
|
||||
lnHTTPS, err := net.Listen("tcp", net.JoinHostPort(conf.DNSServeIP, "443"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen port")
|
||||
}
|
||||
go ServeHTTPS(lnHTTPS, r)
|
||||
|
||||
if err := dns.ListenAndServe(conf.DNSServeIP, "udp", r); err != nil {
|
||||
log.Fatal().Err(err).Msg("serve dns")
|
||||
}
|
||||
}()
|
||||
|
||||
ln, err := net.Listen("tcp", conf.Socks5Addr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen port")
|
||||
}
|
||||
go ServeSocks5(ln, r)
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func genProxyDial() func(network, host string, port uint16) (net.Conn, error) {
|
||||
var (
|
||||
proxyAddr = net.JoinHostPort(conf.Proxy.Addr, "443")
|
||||
tlsCfg = &tls.Config{}
|
||||
proxy transport.Transport
|
||||
)
|
||||
switch conf.Proxy.Type {
|
||||
case "sower":
|
||||
proxy = sower.New(conf.Proxy.Password)
|
||||
case "trojan":
|
||||
proxy = trojan.New(conf.Proxy.Password)
|
||||
default:
|
||||
log.Fatal().
|
||||
Str("type", conf.Proxy.Type).
|
||||
Msg("unknown proxy type")
|
||||
}
|
||||
|
||||
return func(network, host string, port uint16) (net.Conn, error) {
|
||||
if host == "" || port == 0 {
|
||||
return nil, errors.Errorf("invalid addr(%s:%d)", host, port)
|
||||
}
|
||||
|
||||
c, err := tls.Dial("tcp", proxyAddr, tlsCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := proxy.Wrap(c, host, port); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ServeHTTP(ln net.Listener, r *router.Router) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Msg("serve socks5")
|
||||
}
|
||||
|
||||
go ServeHTTP(ln, r)
|
||||
start := time.Now()
|
||||
teeconn := teeconn.New(conn)
|
||||
defer teeconn.Close()
|
||||
|
||||
req, err := http.ReadRequest(bufio.NewReader(teeconn))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read http request")
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := r.ProxyDial("tcp", req.Host, 80)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("host", req.Host).
|
||||
Interface("req", req.URL).
|
||||
Msg("dial proxy")
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
teeconn.Stop().Reread()
|
||||
util.Relay(teeconn, rc)
|
||||
log.Info().
|
||||
Str("host", req.Host).
|
||||
Dur("spend", time.Since(start)).
|
||||
Msg("serve http")
|
||||
}
|
||||
|
||||
func ServeHTTPS(ln net.Listener, r *router.Router) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Msg("serve socks5")
|
||||
}
|
||||
|
||||
go ServeHTTPS(ln, r)
|
||||
start := time.Now()
|
||||
teeconn := teeconn.New(conn)
|
||||
defer teeconn.Close()
|
||||
|
||||
var domain string
|
||||
tls.Server(teeconn, &tls.Config{
|
||||
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
domain = hello.ServerName
|
||||
return nil, nil
|
||||
},
|
||||
}).Handshake()
|
||||
|
||||
rc, err := r.ProxyDial("tcp", domain, 443)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("host", domain).
|
||||
Msg("dial proxy")
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
teeconn.Stop().Reread()
|
||||
util.Relay(teeconn, rc)
|
||||
log.Info().
|
||||
Str("host", domain).
|
||||
Dur("spend", time.Since(start)).
|
||||
Msg("serve http")
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/wweir/sower/router"
|
||||
)
|
||||
|
||||
func ServeSocks5(ln net.Listener, r *router.Router) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Msg("serve socks5")
|
||||
}
|
||||
go ServeSocks5(ln, r)
|
||||
defer conn.Close()
|
||||
start := time.Now()
|
||||
|
||||
{
|
||||
auth := new(socks5AuthReq)
|
||||
if err := auth.Fulfill(conn); err != nil {
|
||||
log.Error().Err(err).
|
||||
Interface("request", auth).
|
||||
Msg("socks5 auth")
|
||||
return
|
||||
}
|
||||
|
||||
if err := binary.Write(conn, binary.BigEndian, socks5AuthResp); err != nil {
|
||||
log.Error().Err(err).
|
||||
Msg("socks5 auth")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var addr addrType
|
||||
{
|
||||
head := new(socks5HeadReq)
|
||||
if err := binary.Read(conn, binary.BigEndian, head); err != nil || !head.IsValid() {
|
||||
return
|
||||
}
|
||||
switch head.ATYP {
|
||||
case 0x01: // IPv4
|
||||
addr = &addrTypeIPv4{}
|
||||
case 0x03: // domain name
|
||||
addr = &addrTypeDomain{}
|
||||
case 0x04: // IPv6
|
||||
addr = &addrTypeIPv6{}
|
||||
default:
|
||||
log.Error().Err(err).
|
||||
Interface("head", head).
|
||||
Msg("socks5 connect")
|
||||
return
|
||||
}
|
||||
|
||||
if err := addr.Fulfill(conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := binary.Write(conn, binary.BigEndian, socks5HeadResp); err != nil {
|
||||
log.Error().Err(err).
|
||||
Msg("socks5 head")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
host, port := addr.Addr()
|
||||
log.Err(r.RouteHandle(conn, host, port)).
|
||||
Str("host", host).
|
||||
Uint16("port", port).
|
||||
Dur("spend", time.Since(start)).
|
||||
Msg("serve socsk5")
|
||||
}
|
||||
|
||||
/******************* https://tools.ietf.org/html/rfc1928 *******************/
|
||||
|
||||
// 1. client send auth request
|
||||
type socks5AuthReq struct {
|
||||
VER byte
|
||||
NMETHODS uint8
|
||||
METHODS []byte
|
||||
}
|
||||
|
||||
func (req *socks5AuthReq) Fulfill(r io.Reader) error {
|
||||
buf := make([]byte, 2)
|
||||
if n, err := r.Read(buf); err != nil || n != 2 {
|
||||
return err
|
||||
}
|
||||
|
||||
req.VER = buf[0]
|
||||
req.NMETHODS = buf[1]
|
||||
|
||||
req.METHODS = make([]byte, int(req.NMETHODS))
|
||||
if n, err := r.Read(req.METHODS); err != nil || n != len(req.METHODS) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *socks5AuthReq) IsValid() bool {
|
||||
return r.VER == 5 && r.METHODS[0] == 0
|
||||
}
|
||||
|
||||
// 2. server response auth request
|
||||
var socks5AuthResp = struct {
|
||||
VER byte
|
||||
METHOD byte
|
||||
}{VER: 5, METHOD: 0}
|
||||
|
||||
// 3. client request with target address
|
||||
type socks5HeadReq struct {
|
||||
VER byte
|
||||
CMD byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
}
|
||||
|
||||
func (r *socks5HeadReq) IsValid() bool {
|
||||
return r.VER == 5 && r.CMD == 1
|
||||
}
|
||||
|
||||
// 4. server response with the address that assigned to connect to target address
|
||||
var socks5HeadResp = struct {
|
||||
VER byte
|
||||
REP byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
BIND struct {
|
||||
ADDR [4]byte
|
||||
PORT uint16
|
||||
}
|
||||
}{VER: 5, REP: 0, RSV: 0, ATYP: 1}
|
||||
|
||||
type addrType interface {
|
||||
Fulfill(r io.Reader) error
|
||||
Addr() (domain string, port uint16)
|
||||
}
|
||||
|
||||
// ATYP:
|
||||
// 0x01 -> net.IPv4len
|
||||
// 0x03 -> first byte is length
|
||||
// 0x04 -> net.IPv6len
|
||||
type addrTypeIPv4 struct {
|
||||
DST_ADDR [4]byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
func (a *addrTypeIPv4) Fulfill(r io.Reader) error {
|
||||
return binary.Read(r, binary.BigEndian, &a)
|
||||
}
|
||||
func (a *addrTypeIPv4) Addr() (string, uint16) {
|
||||
return net.IP(a.DST_ADDR[:]).String(), a.DST_PORT
|
||||
}
|
||||
|
||||
type addrTypeIPv6 struct {
|
||||
DST_ADDR [16]byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
func (a *addrTypeIPv6) Fulfill(r io.Reader) error {
|
||||
return binary.Read(r, binary.BigEndian, &a)
|
||||
}
|
||||
func (a *addrTypeIPv6) Addr() (string, uint16) {
|
||||
return net.IP(a.DST_ADDR[:]).String(), a.DST_PORT
|
||||
}
|
||||
|
||||
type addrTypeDomain struct {
|
||||
DST_ADDR_LEN uint8
|
||||
DST_ADDR []byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
func (a *addrTypeDomain) Fulfill(r io.Reader) error {
|
||||
buf := make([]byte, 1)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.DST_ADDR_LEN = uint8(buf[0])
|
||||
buf = make([]byte, a.DST_ADDR_LEN+2)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.DST_ADDR = buf[:int(a.DST_ADDR_LEN)]
|
||||
a.DST_PORT = binary.BigEndian.Uint16(buf[int(a.DST_ADDR_LEN):])
|
||||
|
||||
return nil
|
||||
}
|
||||
func (a *addrTypeDomain) Addr() (string, uint16) {
|
||||
return string(a.DST_ADDR[:]), a.DST_PORT
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cristalhq/aconfig"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
"github.com/wweir/sower/pkg/teeconn"
|
||||
"github.com/wweir/sower/transport/sower"
|
||||
"github.com/wweir/sower/transport/trojan"
|
||||
"github.com/wweir/sower/util"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
var (
|
||||
version, date string
|
||||
|
||||
conf = struct {
|
||||
ServeIP string `usage:"listen to port 80 443 of this IP, eg: 0.0.0.0"`
|
||||
Password string `required:"true"`
|
||||
FakeSite string `required:"true" default:"127.0.0.1:8080" usage:"fake site address"`
|
||||
|
||||
Cert struct {
|
||||
Email string
|
||||
Cert string
|
||||
Key string
|
||||
}
|
||||
}{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.ErrorStackMarshaler = func(err error) interface{} {
|
||||
return pkgerrors.MarshalStack(err)
|
||||
}
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: time.StampMilli,
|
||||
FormatCaller: func(i interface{}) string {
|
||||
caller := i.(string)
|
||||
if idx := strings.Index(caller, "/pkg/mod/"); idx > 0 {
|
||||
return caller[idx+9:]
|
||||
}
|
||||
if idx := strings.LastIndexByte(caller, '/'); idx > 0 {
|
||||
return caller[idx+1:]
|
||||
}
|
||||
return caller
|
||||
},
|
||||
}).With().Timestamp().Caller().Logger()
|
||||
|
||||
if err := aconfig.LoaderFor(&conf, aconfig.Config{}).Load(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Load config")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("version", version).
|
||||
Str("date", date).
|
||||
Interface("config", conf).
|
||||
Msg("Starting")
|
||||
}
|
||||
|
||||
func main() {
|
||||
cacheDir, _ := os.UserCacheDir()
|
||||
cacheDir = filepath.Join(cacheDir, "sower")
|
||||
if err := os.MkdirAll(cacheDir, 0600); err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Str("dir", cacheDir).
|
||||
Msg("make cache dir")
|
||||
}
|
||||
|
||||
certManager := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Email: conf.Cert.Email,
|
||||
Cache: autocert.DirCache(cacheDir),
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
GetCertificate: certManager.GetCertificate,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
}
|
||||
if conf.Cert.Cert != "" || conf.Cert.Key != "" {
|
||||
cert, err := tls.LoadX509KeyPair(conf.Cert.Cert, conf.Cert.Key)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("load certificate")
|
||||
}
|
||||
|
||||
tlsConf.GetCertificate = nil
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
// Redirect 80 to 443
|
||||
go http.ListenAndServe(net.JoinHostPort(conf.ServeIP, "80"),
|
||||
certManager.HTTPHandler(http.HandlerFunc(redirectToHTTPS)))
|
||||
|
||||
ln, err := tls.Listen("tcp", net.JoinHostPort(conf.ServeIP, "443"), tlsConf)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen tcp 443")
|
||||
}
|
||||
|
||||
go serve443(ln, conf.FakeSite, sower.New(conf.Password), trojan.New(conf.Password))
|
||||
select {}
|
||||
}
|
||||
|
||||
func redirectToHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Scheme = "https"
|
||||
if host, _, err := net.SplitHostPort(r.Host); err != nil {
|
||||
r.URL.Host = r.Host
|
||||
} else {
|
||||
r.URL.Host = host
|
||||
}
|
||||
|
||||
http.Redirect(w, r, r.URL.String(), 301)
|
||||
}
|
||||
|
||||
func serve443(ln net.Listener, fakeSite string, sower *sower.Sower, trojan *trojan.Trojan) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("serve 443 port")
|
||||
}
|
||||
|
||||
go serve443(ln, fakeSite, sower, trojan)
|
||||
teeconn := teeconn.New(conn)
|
||||
defer teeconn.Close()
|
||||
|
||||
teeconn.Reread()
|
||||
if addr := sower.Unwrap(teeconn); addr != nil {
|
||||
teeconn.Stop()
|
||||
|
||||
dur, err := util.RelayTo(teeconn, addr.String())
|
||||
log.Err(err).
|
||||
Dur("spend", dur).
|
||||
Str("target", addr.String()).
|
||||
Msg("relay sower conn")
|
||||
return
|
||||
}
|
||||
|
||||
teeconn.Reread()
|
||||
if addr := trojan.Unwrap(teeconn); addr != nil {
|
||||
teeconn.Stop()
|
||||
|
||||
dur, err := util.RelayTo(teeconn, addr.String())
|
||||
log.Err(err).
|
||||
Dur("spend", dur).
|
||||
Str("target", addr.String()).
|
||||
Msg("relay trojan conn")
|
||||
return
|
||||
}
|
||||
|
||||
teeconn.Stop().Reread()
|
||||
dur, err := util.RelayTo(teeconn, fakeSite)
|
||||
log.Err(err).
|
||||
Dur("spend", dur).
|
||||
Str("target", fakeSite).
|
||||
Msg("relay fake site")
|
||||
}
|
||||
-196
@@ -1,196 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
toml "github.com/pelletier/go-toml"
|
||||
"github.com/wweir/sower/util"
|
||||
"github.com/wweir/util-go/log"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Address string `toml:"address"`
|
||||
DNSUpstream string `toml:"dns_upstream"`
|
||||
Socks5Proxy string `toml:"socks5"`
|
||||
HTTPProxy string `toml:"http_proxy"`
|
||||
PortForward map[string]string `toml:"port_forward"`
|
||||
|
||||
Router struct {
|
||||
DetectLevel int `toml:"detect_level"`
|
||||
ProxyList []string `toml:"proxy_list"`
|
||||
ProxyRefs []string `toml:"proxy_refs"`
|
||||
DirectList []string `toml:"direct_list"`
|
||||
DirectRefs []string `toml:"direct_refs"`
|
||||
BlockList []string `toml:"block_list"`
|
||||
BlockRefs []string `toml:"block_refs"`
|
||||
} `toml:"router"`
|
||||
}
|
||||
type Server struct {
|
||||
Upstream string `toml:"upstream"`
|
||||
CertFile string `toml:"cert_file"`
|
||||
KeyFile string `toml:"key_file"`
|
||||
CertEmail string `toml:"cert_email"`
|
||||
}
|
||||
|
||||
var (
|
||||
version, date string
|
||||
|
||||
execFile, _ = os.Executable()
|
||||
execDir, _ = filepath.Abs(filepath.Dir(execFile))
|
||||
// Conf full config, include common and server / client
|
||||
conf = struct {
|
||||
file string
|
||||
Password string `toml:"password"`
|
||||
Client Client `toml:"client"`
|
||||
Server Server `toml:"server"`
|
||||
}{}
|
||||
)
|
||||
|
||||
func Init() (*Client, *Server, string) {
|
||||
beforeInitFlag()
|
||||
defer afterInitFlag()
|
||||
flag.StringVar(&conf.Password, "password", "", "password")
|
||||
flag.StringVar(&conf.Server.Upstream, "s", "", "upstream http service, eg: 127.0.0.1:8080")
|
||||
flag.StringVar(&conf.Server.CertFile, "s_cert", "", "tls cert file, gen cert from letsencrypt if empty")
|
||||
flag.StringVar(&conf.Server.KeyFile, "s_key", "", "tls key file, gen cert from letsencrypt if empty")
|
||||
flag.StringVar(&conf.Client.Address, "c", "", "remote server domain, eg: aa.bb.cc, socks5h://127.0.0.1:1080")
|
||||
flag.StringVar(&conf.Client.HTTPProxy, "http_proxy", ":8080", "http proxy, empty to disable")
|
||||
|
||||
if !flag.Parsed() {
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
defer log.Infow("starting", "version", version, "date", date, "config", &conf)
|
||||
if conf.file == "" {
|
||||
return &conf.Client, &conf.Server, conf.Password
|
||||
}
|
||||
|
||||
for i := range loadConfigFns {
|
||||
if err := loadConfigFns[i].fn(); err != nil {
|
||||
log.Fatalw("load config", "config", conf.file, "step", loadConfigFns[i].step, "err", err)
|
||||
}
|
||||
}
|
||||
return &conf.Client, &conf.Server, conf.Password
|
||||
}
|
||||
|
||||
// refreshFns will be executed while init and write new config
|
||||
var loadConfigFns = []struct {
|
||||
step string
|
||||
fn func() error
|
||||
}{{"parse file", func() error {
|
||||
f, err := os.OpenFile(conf.file, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return toml.NewDecoder(f).Decode(&conf)
|
||||
}}, {"load referenced rule", func() error {
|
||||
for _, addr := range conf.Client.Router.BlockRefs {
|
||||
lines, err := getRemoteRuleLines(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf.Client.Router.BlockList = append(conf.Client.Router.BlockList, lines...)
|
||||
}
|
||||
for _, addr := range conf.Client.Router.ProxyRefs {
|
||||
lines, err := getRemoteRuleLines(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf.Client.Router.ProxyList = append(conf.Client.Router.ProxyList, lines...)
|
||||
}
|
||||
for _, addr := range conf.Client.Router.DirectRefs {
|
||||
lines, err := getRemoteRuleLines(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf.Client.Router.DirectList = append(conf.Client.Router.DirectList, lines...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}}}
|
||||
|
||||
func getRemoteRuleLines(addr string) ([]string, error) {
|
||||
resp, err := http.Get(addr)
|
||||
if err != nil {
|
||||
return nil, xerrors.New(err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
br := bufio.NewReader(resp.Body)
|
||||
lines := []string{}
|
||||
for {
|
||||
line, _, err := br.ReadLine()
|
||||
if err == io.EOF {
|
||||
return lines, nil
|
||||
} else if err != nil {
|
||||
return nil, xerrors.New(err.Error())
|
||||
}
|
||||
|
||||
lines = append(lines, "**."+strings.TrimSpace(string(line)))
|
||||
}
|
||||
}
|
||||
|
||||
// flushCh to avoid parallel persist
|
||||
var flushCh = make(chan struct{})
|
||||
var flushOnce = sync.Once{}
|
||||
|
||||
// PersistRule persist rule into config file
|
||||
func PersistRule(domain string) {
|
||||
flushOnce.Do(func() {
|
||||
go flushConfDaemon()
|
||||
})
|
||||
|
||||
log.Infow("persist direct rule into config", "domain", domain)
|
||||
conf.Client.Router.DirectList = append(conf.Client.Router.DirectList, domain)
|
||||
select {
|
||||
case flushCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
func flushConfDaemon() {
|
||||
for range flushCh {
|
||||
// safe write file
|
||||
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
|
||||
}
|
||||
|
||||
conf.Client.Router.DirectList =
|
||||
util.NewReverseSecSlice(conf.Client.Router.DirectList).Sort().Uniq()
|
||||
|
||||
if err := toml.NewEncoder(f).ArraysWithOneElementPerLine(true).Encode(&conf); err != nil {
|
||||
log.Errorw("flush config", "step", "flush", "err", err)
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
f.Close()
|
||||
|
||||
if stat, err := os.Stat(conf.file); err != nil {
|
||||
log.Warnw("get file stat", "file", conf.file, "err", err)
|
||||
} else {
|
||||
// There is no common way to transfer ownership for a file
|
||||
// cross-platform. Drop the ownership support but file mod.
|
||||
if err = os.Chmod(conf.file+"~", stat.Mode()); err != nil {
|
||||
log.Warnw("set file mod", "file", conf.file+"~", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.Rename(conf.file+"~", conf.file); err != nil {
|
||||
log.Errorw("flush config", "step", "flush", "err", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// +build darwin
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/util-go/log"
|
||||
)
|
||||
|
||||
const svcPath = "/Library/LaunchDaemons/sower.plist"
|
||||
const svcFile = `<?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>sower</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-c</string>
|
||||
<string>%s %s</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>`
|
||||
|
||||
var (
|
||||
ConfigDir = ""
|
||||
installCmd = ""
|
||||
uninstallFlag = false
|
||||
)
|
||||
|
||||
func beforeInitFlag() {
|
||||
if _, err := os.Stat(execDir + "/sower.toml"); err == nil {
|
||||
ConfigDir = execDir
|
||||
} else {
|
||||
dir, _ := os.UserConfigDir()
|
||||
ConfigDir = filepath.Join("/", dir, "sower")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ConfigDir + "/sower.toml"); err != nil {
|
||||
flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set")
|
||||
} else {
|
||||
flag.StringVar(&conf.file, "f", ConfigDir+"/sower.toml", "config file, rewrite all other parameters if set")
|
||||
}
|
||||
|
||||
flag.StringVar(&installCmd, "install", "", "install service with cmd, eg: '-f \""+ConfigDir+"/sower.toml\"'")
|
||||
flag.BoolVar(&uninstallFlag, "uninstall", false, "uninstall service")
|
||||
}
|
||||
|
||||
func afterInitFlag() {
|
||||
switch {
|
||||
case installCmd != "":
|
||||
install()
|
||||
case uninstallFlag:
|
||||
uninstall()
|
||||
default:
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func install() {
|
||||
if err := ioutil.WriteFile(svcPath, []byte(fmt.Sprintf(svcFile, execFile, installCmd)), 0644); err != nil {
|
||||
log.Fatalw("write service file", "err", err)
|
||||
}
|
||||
|
||||
execute("launchctl unload " + svcPath)
|
||||
if err := execute("launchctl load -wF " + svcPath); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstall() {
|
||||
execute("launchctl unload " + svcPath)
|
||||
os.Remove(svcPath)
|
||||
os.RemoveAll("/etc/sower")
|
||||
}
|
||||
|
||||
func execute(cmd string) error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmd: %s, err: %s, output: %s", cmd, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// +build linux
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/util-go/log"
|
||||
)
|
||||
|
||||
const svcPath = "/etc/systemd/system/sower.service"
|
||||
const svcFile = `[Unit]
|
||||
Description=Sower client service
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/tmp
|
||||
ExecStart=%s %s
|
||||
RestartSec=3
|
||||
Restart=on-failure`
|
||||
|
||||
var (
|
||||
ConfigDir = ""
|
||||
installCmd = ""
|
||||
uninstallFlag = false
|
||||
)
|
||||
|
||||
func beforeInitFlag() {
|
||||
if _, err := os.Stat(execDir + "/sower.toml"); err == nil {
|
||||
ConfigDir = execDir
|
||||
|
||||
} else if stat, err := os.Stat("/etc/sower"); err == nil && stat.IsDir() {
|
||||
ConfigDir = "/etc/sower"
|
||||
|
||||
} else {
|
||||
dir, _ := os.UserConfigDir()
|
||||
ConfigDir = filepath.Join("/", dir, "sower")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ConfigDir + "/sower.toml"); err != nil {
|
||||
flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set")
|
||||
} else {
|
||||
flag.StringVar(&conf.file, "f", ConfigDir+"/sower.toml", "config file, rewrite all other parameters if set")
|
||||
}
|
||||
|
||||
flag.StringVar(&installCmd, "install", "", "install service with cmd, eg: '-f "+ConfigDir+"/sower.toml'")
|
||||
flag.BoolVar(&uninstallFlag, "uninstall", false, "uninstall service")
|
||||
}
|
||||
|
||||
func afterInitFlag() {
|
||||
switch {
|
||||
case installCmd != "":
|
||||
install()
|
||||
case uninstallFlag:
|
||||
uninstall()
|
||||
default:
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func install() {
|
||||
if err := ioutil.WriteFile(svcPath, []byte(fmt.Sprintf(svcFile, execFile, installCmd)), 0644); err != nil {
|
||||
log.Fatalw("write service file", "err", err)
|
||||
}
|
||||
if err := execute("systemctl daemon-reload"); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
if err := execute("systemctl enable sower"); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
if err := execute("systemctl start sower"); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstall() {
|
||||
execute("systemctl stop sower")
|
||||
execute("systemctl disable sower")
|
||||
os.Remove(svcPath)
|
||||
os.RemoveAll("/etc/sower")
|
||||
}
|
||||
|
||||
func execute(cmd string) error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmd: %s, err: %s, output: %s", cmd, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/util-go/log"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
const name = "sower"
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
|
||||
|
||||
var (
|
||||
ConfigDir = ""
|
||||
installCmd = false
|
||||
uninstallFlag = false
|
||||
)
|
||||
|
||||
func beforeInitFlag() {
|
||||
flag.StringVar(&conf.file, "f", filepath.Join(execDir, "sower.toml"), "config file, rewrite all other parameters if set")
|
||||
flag.BoolVar(&installCmd, "install", false, "put any character to install as a service, eg: true")
|
||||
}
|
||||
|
||||
func afterInitFlag() {
|
||||
switch {
|
||||
case installCmd:
|
||||
install()
|
||||
case uninstallFlag:
|
||||
uninstall()
|
||||
default:
|
||||
runAsService()
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func runAsService() {
|
||||
os.Chdir(filepath.Dir(os.Args[0]))
|
||||
if active, err := svc.IsAnInteractiveSession(); err != nil {
|
||||
log.Fatalw("failed to determine if we are running in an interactive session", "err", err)
|
||||
} else if !active {
|
||||
go func() {
|
||||
elog, err := eventlog.Open(name)
|
||||
if err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
defer elog.Close()
|
||||
|
||||
if err := svc.Run(name, &myservice{}); err != nil {
|
||||
elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("winsvc.RunAsService: %s service stopped", name))
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func install() {
|
||||
mgrDo(func(m *mgr.Mgr) error {
|
||||
s, err := m.OpenService(name)
|
||||
if err == nil {
|
||||
s.Close()
|
||||
return fmt.Errorf("service %s already exists", name)
|
||||
}
|
||||
s, err = m.CreateService(name, execFile, mgr.Config{
|
||||
DisplayName: "Sower Proxy",
|
||||
StartType: windows.SERVICE_AUTO_START,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
|
||||
}
|
||||
|
||||
return s.Start()
|
||||
})
|
||||
}
|
||||
|
||||
func uninstall() {
|
||||
serviceDo(func(s *mgr.Service) error {
|
||||
err := s.Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return eventlog.Remove(name)
|
||||
})
|
||||
}
|
||||
|
||||
func serviceDo(fn func(*mgr.Service) error) {
|
||||
mgrDo(func(m *mgr.Mgr) error {
|
||||
s, err := m.OpenService(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not access service: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
return fn(s)
|
||||
})
|
||||
}
|
||||
func mgrDo(fn func(m *mgr.Mgr) error) {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
if err := fn(m); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
type myservice struct{}
|
||||
|
||||
func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
elog, err := eventlog.Open(name)
|
||||
if err != nil {
|
||||
log.Errorw("install service", "err", err)
|
||||
return
|
||||
}
|
||||
defer elog.Close()
|
||||
elog.Info(1, strings.Join(args, "-"))
|
||||
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
for {
|
||||
c := <-r
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
changes <- c.CurrentStatus
|
||||
// Testing deadlock from https://code.google.com/p/winsvc/issues/detail?id=4
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return
|
||||
case svc.Pause:
|
||||
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
|
||||
case svc.Continue:
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
default:
|
||||
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func execute(cmd string) error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmds []string
|
||||
for _, cmd := range strings.Split(cmd, " ") {
|
||||
if cmd == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cmd, "/") {
|
||||
cmd = strings.Replace(cmd, "/", "-", 1)
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
if len(cmds) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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", cmd, out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
password = "" # sower password
|
||||
|
||||
[client]
|
||||
address = "" # aa.bb.cc, socks5h://127.0.0.1:1080
|
||||
dns_upstream = "" # keep empty to set via dhcp, not effective in any environment
|
||||
http_proxy = ""
|
||||
socks5 = ""
|
||||
|
||||
[client.port_forward]
|
||||
# eg: ":2222"="aa.bb.cc:22"
|
||||
|
||||
[client.router]
|
||||
block_refs = [
|
||||
"https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/adlist/adlist.txt",
|
||||
]
|
||||
detect_level = 0 # [-4, 4], the bigger the harder to add
|
||||
direct_cird_refs = [
|
||||
"https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chnroute/chnroute.txt",
|
||||
]
|
||||
direct_list = [
|
||||
"**.in-addr.arpa",
|
||||
"imap.*.*",
|
||||
"imap.*.*.*",
|
||||
"smtp.*.*",
|
||||
"smtp.*.*.*",
|
||||
"pop.*.*",
|
||||
"pop.*.*.*",
|
||||
"**.cn",
|
||||
]
|
||||
direct_refs = [
|
||||
"https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chinalist/chinalist.txt",
|
||||
]
|
||||
proxy_list = [
|
||||
"**.google.*",
|
||||
"**.goo.gl",
|
||||
"**.googleusercontent.com",
|
||||
"**.googleapis.com",
|
||||
"*.googlesource.com",
|
||||
"**.youtube.com",
|
||||
"**.ytimg.com",
|
||||
"**.ggpht.com",
|
||||
"**.googlevideo.com",
|
||||
"**.facebook.com",
|
||||
"**.fbcdn.net",
|
||||
"**.twitter.com",
|
||||
"**.twimg.com",
|
||||
"**.blogspot.com",
|
||||
"**.appspot.com",
|
||||
"**.wikipedia.org",
|
||||
"*.cloudfront.net",
|
||||
"**.amazon.com",
|
||||
"**.amazonaws.com",
|
||||
"*.githubusercontent.com",
|
||||
"*.githubassets.com",
|
||||
"*.github.*",
|
||||
]
|
||||
proxy_refs = [
|
||||
"https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/gfwlist/gfwlist.txt",
|
||||
]
|
||||
|
||||
[server]
|
||||
cert_email = "" # eg: user@aa.bb.cc
|
||||
cert_file = "" # eg: /etc/ssl/server.crt
|
||||
key_file = "" # eg: /etc/ssl/server.key
|
||||
upstream = "" # eg: 127.0.0.1:8080
|
||||
@@ -3,14 +3,16 @@ module github.com/wweir/sower
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/cristalhq/aconfig v0.16.1
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771
|
||||
github.com/libp2p/go-reuseport v0.0.2
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/pelletier/go-toml v1.8.0
|
||||
github.com/oschwald/geoip2-golang v1.5.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/wweir/util-go/log v0.0.0-20200701032032-3cff7b4a46ea
|
||||
github.com/wweir/util-go/mem v0.0.0-20200701032032-3cff7b4a46ea
|
||||
github.com/rs/zerolog v1.23.0
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cristalhq/aconfig v0.16.1 h1:P3noQOujaPU5Of8E9fA1YYU3s/HUoPCIhpKkEgIQtfc=
|
||||
github.com/cristalhq/aconfig v0.16.1/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk=
|
||||
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
|
||||
@@ -12,64 +16,61 @@ github.com/libp2p/go-reuseport v0.0.2 h1:XSG94b1FJfGA01BUrT82imejHQyTxO4jEWqheyC
|
||||
github.com/libp2p/go-reuseport v0.0.2/go.mod h1:SPD+5RwGC7rcnzngoYC86GjPzjSywuQyMVAheVBD9nQ=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
|
||||
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/oschwald/geoip2-golang v1.5.0 h1:igg2yQIrrcRccB1ytFXqBfOHCjXWIoMv85lVJ1ONZzw=
|
||||
github.com/oschwald/geoip2-golang v1.5.0/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s=
|
||||
github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk=
|
||||
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.23.0 h1:UskrK+saS9P9Y789yNNulYKdARjPZuS35B8gJF2x60g=
|
||||
github.com/rs/zerolog v1.23.0/go.mod h1:6c7hFfxPOy7TacJc4Fcdi24/J0NKYGzjG8FWRI916Qo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6 h1:TtyC78WMafNW8QFfv3TeP3yWNDG+uxNkk9vOrnDu6JA=
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6/go.mod h1:h8272+G2omSmi30fBXiZDMkmHuOgonplfKIKjQWzlfs=
|
||||
github.com/wweir/util-go v0.0.0-20200701032032-3cff7b4a46ea h1:V/bIKmS5Bv4zMwQ5qE7ugJiHpcAVFnAxvxRbb7OCwug=
|
||||
github.com/wweir/util-go/log v0.0.0-20200701032032-3cff7b4a46ea h1:IlO7d9R0I/l2syxJkx4/PURW9yVf4hy/Qf2IzdR4y38=
|
||||
github.com/wweir/util-go/log v0.0.0-20200701032032-3cff7b4a46ea/go.mod h1:OObmMboiahxgJ1P3twG+AN8EAZ/wz14vstI4E5kaZzI=
|
||||
github.com/wweir/util-go/mem v0.0.0-20200701032032-3cff7b4a46ea h1:nXgUp7CflDJnuR+zd43DHKxRA/hvuMkFtueSRLY7mEc=
|
||||
github.com/wweir/util-go/mem v0.0.0-20200701032032-3cff7b4a46ea/go.mod h1:gp1gaDO1K4uV/B8NfrTjVim99Xq5xAmm1SHVamRsVW0=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
|
||||
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/conf"
|
||||
"github.com/wweir/sower/proxy"
|
||||
"github.com/wweir/sower/router"
|
||||
"github.com/wweir/sower/transport"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, server, password := conf.Init()
|
||||
|
||||
switch {
|
||||
case server.Upstream != "":
|
||||
proxy.StartServer(server.Upstream, password, conf.ConfigDir,
|
||||
server.CertFile, server.KeyFile, server.CertEmail)
|
||||
|
||||
case client.Address != "":
|
||||
route := router.NewRoute(client.Address, password, client.Router.DetectLevel,
|
||||
client.Router.BlockList, client.Router.ProxyList, client.Router.DirectList,
|
||||
conf.PersistRule)
|
||||
|
||||
if client.Socks5Proxy != "" {
|
||||
go proxy.StartSocks5Proxy(client.Socks5Proxy, client.Address, []byte(password))
|
||||
}
|
||||
|
||||
if client.HTTPProxy != "" {
|
||||
go proxy.StartHTTPProxy(client.HTTPProxy, client.Address,
|
||||
[]byte(password), route.GenProxyCheck(true))
|
||||
}
|
||||
|
||||
transport.SetDNS(nil, client.DNSUpstream)
|
||||
go proxy.StartDNS(client.DNSUpstream, route.GenProxyCheck(false))
|
||||
|
||||
proxy.StartClient(client.Address, password,
|
||||
client.PortForward, route.GenProxyCheck(true))
|
||||
|
||||
default:
|
||||
fmt.Println()
|
||||
flag.Usage()
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
var xid = make([]byte, 4)
|
||||
var broadcastAddr, _ = net.ResolveUDPAddr("udp", "255.255.255.255:67")
|
||||
|
||||
func GetDefaultDNSServer() (string, error) {
|
||||
func GetDNSServer() (string, error) {
|
||||
iface, err := PickInternetInterface()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "pick interface")
|
||||
@@ -3,11 +3,11 @@ package dhcp_test
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/wweir/sower/pkg/dhcp"
|
||||
)
|
||||
|
||||
func Example_dns() {
|
||||
got, err := dhcp.GetDefaultDNSServer()
|
||||
got, err := dhcp.GetDNSServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package dhcp_test
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/wweir/sower/pkg/dhcp"
|
||||
)
|
||||
|
||||
func Example_iface() {
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package mem
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
// Data define the type which can speed up by mem cache
|
||||
type Data interface {
|
||||
Fulfill(key interface{}) error
|
||||
}
|
||||
|
||||
// Cache is the definition of cache, be careful of the memory usage
|
||||
type Cache struct {
|
||||
old *sync.Map
|
||||
now *sync.Map
|
||||
barrier *sync.Map
|
||||
rotate <-chan time.Time
|
||||
rwmutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// DefaultCache is default cache for surge
|
||||
var DefaultCache = New(time.Minute)
|
||||
|
||||
// Remember is a surge, it provides a quite simple way to use cache
|
||||
func Remember(dst Data, key interface{}) error {
|
||||
return DefaultCache.Remember(dst, key)
|
||||
}
|
||||
|
||||
// Delete is a surge, it delete a specified data in DefaultCache
|
||||
func Delete(dst Data, key interface{}) {
|
||||
DefaultCache.Delete(dst, key)
|
||||
}
|
||||
|
||||
// New create a cache entity with a custom expiration time
|
||||
func New(rotateInterval time.Duration) *Cache {
|
||||
return &Cache{
|
||||
old: &sync.Map{},
|
||||
now: &sync.Map{},
|
||||
barrier: &sync.Map{},
|
||||
rotate: time.NewTicker(rotateInterval).C,
|
||||
rwmutex: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Remember automatically save and retrieve data from a cache entity
|
||||
func (c *Cache) Remember(dst Data, key interface{}) error {
|
||||
rv := reflect.ValueOf(dst)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
panic("invalid not pointor type: " + reflect.TypeOf(dst).Name())
|
||||
} else if rv.IsNil() {
|
||||
return errors.New("invalid nil pointor")
|
||||
}
|
||||
|
||||
c.rwmutex.RLock()
|
||||
defer c.rwmutex.RUnlock()
|
||||
|
||||
// rotate logic, rwlock just protect fields in Cache, but not field content.
|
||||
// So that, write lock just take a very short time, and simple read lock is
|
||||
// just an atomic action, do not care the performance
|
||||
select {
|
||||
case <-c.rotate:
|
||||
c.old = c.now
|
||||
c.now = &sync.Map{}
|
||||
c.barrier = &sync.Map{}
|
||||
default:
|
||||
}
|
||||
|
||||
// First: load from cache
|
||||
cacheKey := fmt.Sprintf("%T%v", dst, key)
|
||||
if val, ok := c.now.Load(cacheKey); ok {
|
||||
return deepcopier.Copy(val).To(dst)
|
||||
}
|
||||
|
||||
// Second: load from old cache, or waitting the sigle groutine getting data
|
||||
ch := make(chan struct{})
|
||||
if chVal, ok := c.barrier.LoadOrStore(cacheKey, ch); ok {
|
||||
close(ch) // the ch is not used
|
||||
|
||||
if val, ok := c.old.Load(cacheKey); ok {
|
||||
return deepcopier.Copy(val).To(dst)
|
||||
}
|
||||
|
||||
// type chan: wait the sigle groutine getting data
|
||||
// type error: already failed
|
||||
if ch, ok = chVal.(chan struct{}); ok {
|
||||
<-ch
|
||||
if val, ok := c.now.Load(cacheKey); ok {
|
||||
return deepcopier.Copy(val).To(dst)
|
||||
}
|
||||
}
|
||||
|
||||
val, _ := c.barrier.Load(cacheKey)
|
||||
if err, ok := val.(error); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
panic("new value lost, please report a bug")
|
||||
}
|
||||
|
||||
// Third: getting data from CacheType, maybe from db
|
||||
err := dst.Fulfill(key)
|
||||
if err != nil {
|
||||
c.barrier.Store(cacheKey, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.now.Store(cacheKey, dst)
|
||||
close(ch) // broadcast, wakeup all waiting groutine
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete immediately specified the cached content to expire
|
||||
func (c *Cache) Delete(dst Data, key interface{}) {
|
||||
c.rwmutex.Lock()
|
||||
defer c.rwmutex.Unlock()
|
||||
|
||||
cacheKey := fmt.Sprintf("%T%v", dst, key)
|
||||
c.old.Delete(cacheKey)
|
||||
c.now.Delete(cacheKey)
|
||||
c.barrier.Store(cacheKey, errors.New(cacheKey+"is deleted"))
|
||||
}
|
||||
|
||||
// Rotate force refresh cached data
|
||||
func (c *Cache) Rotate(reset bool) {
|
||||
c.rwmutex.Lock()
|
||||
defer c.rwmutex.Unlock()
|
||||
|
||||
if reset {
|
||||
c.old = &sync.Map{}
|
||||
} else {
|
||||
c.old = c.now
|
||||
}
|
||||
c.now = &sync.Map{}
|
||||
c.barrier = &sync.Map{}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package teeconn
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
|
||||
buf []byte
|
||||
offset int
|
||||
stop bool // read
|
||||
err error
|
||||
}
|
||||
|
||||
func New(c net.Conn) *Conn {
|
||||
return &Conn{Conn: c}
|
||||
}
|
||||
|
||||
func (t *Conn) Reread() {
|
||||
t.offset = 0
|
||||
}
|
||||
func (t *Conn) Reset() {
|
||||
t.buf = []byte{}
|
||||
t.offset = 0
|
||||
}
|
||||
func (t *Conn) Stop() *Conn {
|
||||
t.stop = true
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Conn) 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, t.err
|
||||
}
|
||||
|
||||
n, t.err = t.Conn.Read(b)
|
||||
if !t.stop {
|
||||
t.buf = append(t.buf, b[:n]...)
|
||||
t.offset += n
|
||||
}
|
||||
|
||||
return n, t.err
|
||||
}
|
||||
|
||||
func (t *Conn) Write(b []byte) (n int, err error) {
|
||||
if t.stop {
|
||||
return t.Conn.Write(b)
|
||||
}
|
||||
|
||||
return 0, io.ErrShortWrite
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/wweir/util-go/log"
|
||||
)
|
||||
|
||||
type msgCache struct {
|
||||
*dns.Msg
|
||||
time.Time
|
||||
}
|
||||
|
||||
var cache sync.Map
|
||||
|
||||
func StartDNS(relayServer string, shouldProxy func(string) (bool, bool)) {
|
||||
var err error
|
||||
if relayServer, err = pickRelayAddr(relayServer); err != nil {
|
||||
log.Fatalw("pick upstream dns server", "err", err)
|
||||
}
|
||||
log.Infow("upstream dns", "addr", relayServer)
|
||||
|
||||
dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// *Msg r has an TSIG record and it was validated
|
||||
if r.IsTsig() != nil && w.TsigStatus() == nil {
|
||||
lastTsig := r.Extra[len(r.Extra)-1].(*dns.TSIG)
|
||||
r.SetTsig(lastTsig.Hdr.Name, dns.HmacMD5, 300, time.Now().Unix())
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
|
||||
if len(r.Question) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
domain := r.Question[0].Name
|
||||
if idx := strings.IndexByte(domain, ':'); idx > 0 {
|
||||
domain = domain[:idx] // trim port
|
||||
}
|
||||
|
||||
if isblock, isproxy := shouldProxy(domain); isblock {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
w.WriteMsg(m)
|
||||
} else if isproxy {
|
||||
host, _, _ := net.SplitHostPort(w.LocalAddr().String())
|
||||
w.WriteMsg(localA(r, domain, net.ParseIP(host)))
|
||||
|
||||
} else if val, ok := cache.Load(domain); ok && val.(*msgCache).After(time.Now()) {
|
||||
msg := val.(*msgCache)
|
||||
for _, rr := range msg.Answer {
|
||||
rr.Header().Ttl = uint32(time.Until(msg.Time).Seconds())
|
||||
}
|
||||
msg.SetReply(r)
|
||||
w.WriteMsg(msg.Msg)
|
||||
|
||||
} else if msg, err := dns.Exchange(r, relayServer); err != nil || msg == nil {
|
||||
cache.Delete(domain)
|
||||
if server, err := pickRelayAddr(relayServer); err != nil {
|
||||
log.Errorw("detect upstream dns", "err", err)
|
||||
} else if relayServer != server {
|
||||
relayServer = server
|
||||
log.Infow("detect upstream dns", "addr", relayServer)
|
||||
}
|
||||
|
||||
} else {
|
||||
cache.Delete(domain)
|
||||
if len(msg.Answer) != 0 {
|
||||
deadline := time.Now().Add(
|
||||
time.Duration(msg.Answer[0].Header().Ttl) * time.Second)
|
||||
cache.Store(domain, &msgCache{
|
||||
Msg: msg,
|
||||
Time: deadline,
|
||||
})
|
||||
}
|
||||
|
||||
w.WriteMsg(msg)
|
||||
}
|
||||
})
|
||||
|
||||
server := &dns.Server{Addr: ":53", Net: "udp"}
|
||||
log.Infow("start dns", "addr", server.Addr)
|
||||
log.Fatalw("dns serve fail", "err", server.ListenAndServe())
|
||||
}
|
||||
|
||||
func pickRelayAddr(relayServer string) (_ string, err error) {
|
||||
if relayServer == "" {
|
||||
if relayServer, err = dhcp.GetDefaultDNSServer(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(relayServer); err != nil {
|
||||
return net.JoinHostPort(relayServer, "53"), nil
|
||||
}
|
||||
return relayServer, nil
|
||||
}
|
||||
|
||||
func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
if localIP.To4() != nil {
|
||||
m.Answer = []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
|
||||
A: localIP,
|
||||
}}
|
||||
} else {
|
||||
m.Answer = []dns.RR{&dns.AAAA{
|
||||
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20},
|
||||
AAAA: localIP,
|
||||
}}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/sower/util"
|
||||
"github.com/wweir/util-go/log"
|
||||
)
|
||||
|
||||
// StartHTTPProxy start http reverse proxy.
|
||||
// The httputil.ReverseProxy do not supply enough support for https request.
|
||||
func StartHTTPProxy(httpProxyAddr, serverAddr string, password []byte,
|
||||
shouldProxy func(string) (bool, bool)) {
|
||||
|
||||
proxy := httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {},
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return transport.Dial(serverAddr, func(host string) (string, []byte) {
|
||||
if _, ok := shouldProxy(host); ok {
|
||||
return httpProxyAddr, password
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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, shouldProxy)
|
||||
} else {
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
}),
|
||||
// Disable HTTP/2.
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
|
||||
IdleTimeout: 90 * time.Second,
|
||||
}
|
||||
|
||||
log.Infow("start sower http proxy", "http_proxy", httpProxyAddr)
|
||||
go log.Fatalw("serve http proxy", "addr", httpProxyAddr, "err", srv.ListenAndServe())
|
||||
}
|
||||
|
||||
func httpsProxy(w http.ResponseWriter, r *http.Request,
|
||||
serverAddr string, password []byte, shouldProxy func(string) (bool, bool)) {
|
||||
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
conn.(*net.TCPConn).SetKeepAlive(true)
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.Write([]byte(r.Proto + " 200 Connection established\r\n\r\n")); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
target, _ := util.WithDefaultPort(r.Host, "443")
|
||||
rc, err := transport.Dial(target, func(host string) (string, []byte) {
|
||||
if _, ok := shouldProxy(host); ok {
|
||||
return serverAddr, password
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
conn.Write([]byte("sower dial " + serverAddr + " fail: " + err.Error()))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
relay(conn, rc)
|
||||
}
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/util-go/log"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func StartClient(serverAddr, password string,
|
||||
forwards map[string]string, shouldProxy func(string) (bool, bool)) {
|
||||
|
||||
passwordData := []byte(password)
|
||||
relayToRemote := func(lnAddr, target string,
|
||||
parseFn func(net.Conn) (net.Conn, string, error),
|
||||
shouldProxy func(string) (bool, bool)) {
|
||||
|
||||
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) {
|
||||
defer conn.Close()
|
||||
|
||||
if parseFn != nil {
|
||||
if conn, target, err = parseFn(conn); err != nil {
|
||||
log.Warnw("parse target", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rc, err := transport.Dial(target, func(domain string) (string, []byte) {
|
||||
if _, ok := shouldProxy(domain); ok {
|
||||
return serverAddr, passwordData
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnw("dial", "addr", target, "err", err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
relay(conn, rc)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
for from, to := range forwards {
|
||||
go relayToRemote(from, to, nil, func(string) (bool, bool) { return false, true })
|
||||
}
|
||||
|
||||
go relayToRemote(":http", "", ParseHTTP, shouldProxy)
|
||||
go relayToRemote(":https", "", ParseHTTPS, shouldProxy)
|
||||
|
||||
log.Infow("start sower client", "forwards", forwards)
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func StartServer(relayTarget, password, cacheDir, certFile, keyFile, email string) {
|
||||
certManager := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Email: email,
|
||||
Cache: autocert.DirCache(cacheDir),
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
GetCertificate: certManager.GetCertificate,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
}
|
||||
if certFile != "" && keyFile != "" {
|
||||
if cert, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil {
|
||||
log.Fatalw("load certificate", "cert", certFile, "key", keyFile, "err", err)
|
||||
} else {
|
||||
tlsConf.GetCertificate = nil
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to redirect 80 to 443
|
||||
go http.ListenAndServe(":80", certManager.HTTPHandler(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Scheme = "https"
|
||||
if host, _, err := net.SplitHostPort(r.Host); err != nil {
|
||||
r.URL.Host = r.Host
|
||||
} else {
|
||||
r.URL.Host = host
|
||||
}
|
||||
|
||||
http.Redirect(w, r, r.URL.String(), 301)
|
||||
})))
|
||||
|
||||
log.Infow("start sower server", "relay_to", relayTarget)
|
||||
ln, err := tls.Listen("tcp", ":443", 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) {
|
||||
defer conn.Close()
|
||||
|
||||
target := relayTarget
|
||||
|
||||
conn, t := transport.ParseTrojanConn(conn, passwordData)
|
||||
if t != nil {
|
||||
target = t.Addr()
|
||||
}
|
||||
|
||||
rc, err := net.Dial("tcp", target)
|
||||
if err != nil {
|
||||
log.Errorw("tcp dial", "addr", target, "err", err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
relay(conn, rc)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/util-go/log"
|
||||
)
|
||||
|
||||
func StartSocks5Proxy(listenAddr, serverAddr string, password []byte) {
|
||||
ln, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
log.Fatalw("socks5 proxy", "addr", listenAddr, "err", err)
|
||||
}
|
||||
|
||||
log.Infow("start socks5 proxy", "endpoint", listenAddr)
|
||||
serveSocks5(ln, serverAddr, password)
|
||||
}
|
||||
|
||||
func serveSocks5(ln net.Listener, serverAddr string, password []byte) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatalw("socks5 proxy", "err", err)
|
||||
}
|
||||
go serveSocks5(ln, serverAddr, password)
|
||||
|
||||
tgtAddr, err := transport.ParseSocks5(conn)
|
||||
if err != nil {
|
||||
log.Errorw("socks5 proxy", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := transport.Dial(tgtAddr, func(host string) (string, []byte) {
|
||||
return serverAddr, password
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorw("socks5 proxy", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
relay(conn, rc)
|
||||
conn.Close()
|
||||
rc.Close()
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/util"
|
||||
)
|
||||
|
||||
func ParseHTTP(conn net.Conn) (net.Conn, string, error) {
|
||||
teeConn := &util.TeeConn{Conn: conn}
|
||||
defer teeConn.Stop()
|
||||
|
||||
resp, err := http.ReadRequest(bufio.NewReader(teeConn))
|
||||
if err != nil {
|
||||
return teeConn, "", err
|
||||
}
|
||||
|
||||
resp.Host, _ = util.WithDefaultPort(resp.Host, "80")
|
||||
return teeConn, resp.Host, nil
|
||||
}
|
||||
|
||||
func ParseHTTPS(conn net.Conn) (net.Conn, string, error) {
|
||||
teeConn := &util.TeeConn{Conn: conn}
|
||||
defer teeConn.Stop()
|
||||
|
||||
var domain string
|
||||
tls.Server(teeConn, &tls.Config{
|
||||
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
domain = hello.ServerName
|
||||
return nil, nil
|
||||
},
|
||||
}).Handshake()
|
||||
|
||||
domain, _ = util.WithDefaultPort(domain, "443")
|
||||
return teeConn, domain, nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
||||
// *Msg r has an TSIG record and it was validated
|
||||
if req.IsTsig() != nil && w.TsigStatus() == nil {
|
||||
lastTsig := req.Extra[len(req.Extra)-1].(*dns.TSIG)
|
||||
req.SetTsig(lastTsig.Hdr.Name, dns.HmacMD5, 300, time.Now().Unix())
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
|
||||
if len(req.Question) == 0 {
|
||||
w.WriteMsg(r.dnsFail(req, dns.RcodeFormatError))
|
||||
return
|
||||
}
|
||||
|
||||
domain := req.Question[0].Name
|
||||
switch {
|
||||
case r.blockRule.Match(domain):
|
||||
w.WriteMsg(r.dnsFail(req, dns.RcodeNameError))
|
||||
return
|
||||
|
||||
case r.directRule.Match(domain):
|
||||
|
||||
case r.proxyRule.Match(domain):
|
||||
host, _, _ := net.SplitHostPort(w.LocalAddr().String())
|
||||
w.WriteMsg(r.dnsProxyA(domain, net.ParseIP(host), req))
|
||||
return
|
||||
}
|
||||
|
||||
conn := <-r.dns.connCh
|
||||
resp, rtt, err := r.dns.ExchangeWithConn(req, conn)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Dur("rtt", rtt).
|
||||
Str("domain", domain).
|
||||
Msg("exchange dns record")
|
||||
|
||||
conn.Close()
|
||||
w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case r.dns.connCh <- conn:
|
||||
default:
|
||||
conn.Close()
|
||||
}
|
||||
w.WriteMsg(resp)
|
||||
}
|
||||
|
||||
func (r *Router) dnsFail(req *dns.Msg, rcode int) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(req, dns.RcodeServerFailure)
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *Router) dnsProxyA(domain string, localIP net.IP, req *dns.Msg) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Port ==========================
|
||||
type Port uint16
|
||||
|
||||
const (
|
||||
HTTP Port = 80
|
||||
HTTPS Port = 443
|
||||
)
|
||||
|
||||
// Ping try connect to a http(s) server with domain though the http addr
|
||||
func (p Port) Ping(domain string, dial func(string) (net.Conn, error)) error {
|
||||
conn, err := dial(net.JoinHostPort(domain, p.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := conn.Write(p.PingMsg(domain)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// err -> nil: read something succ
|
||||
// err -> io.EOF: no such domain or connection refused
|
||||
// err -> timeout: tcp package has been dropped
|
||||
if _, err = conn.Read(make([]byte, 10)); err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Port) String() string {
|
||||
return strconv.Itoa(int(p))
|
||||
}
|
||||
|
||||
func (p Port) PingMsg(domain string) []byte {
|
||||
switch p {
|
||||
case HTTP:
|
||||
return []byte("TRACE / HTTP/1.1\r\nHost: " + domain + "\r\n\r\n")
|
||||
case HTTPS:
|
||||
return NewClientHelloSNIMsg(domain)
|
||||
default:
|
||||
panic("invalid port")
|
||||
}
|
||||
}
|
||||
|
||||
// SNI ==========================
|
||||
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
|
||||
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()
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// +build debug
|
||||
// go test -v -tags debug
|
||||
|
||||
package router_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/router"
|
||||
"github.com/wweir/sower/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
proxyAddr string = "socks5://127.0.0.1:1080"
|
||||
password []byte = nil
|
||||
)
|
||||
|
||||
func TestPort_Ping(t *testing.T) {
|
||||
direct := func(addr string) (net.Conn, error) {
|
||||
return net.DialTimeout("tcp", addr, 5*time.Second)
|
||||
}
|
||||
proxy := func(addr string) (net.Conn, error) {
|
||||
conn, err := tls.Dial("tcp", proxyAddr, &tls.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.ToTrojanConn(conn, host, uint16(p), password)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
domain string
|
||||
dial func(string) (net.Conn, error)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
p router.Port
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"", router.HTTP, args{"baidu.com", direct}, false},
|
||||
{"", router.HTTP, args{"baidu.com", proxy}, false},
|
||||
{"", router.HTTPS, args{"baidu.com", direct}, false},
|
||||
{"", router.HTTPS, args{"baidu.com", proxy}, false},
|
||||
{"", router.HTTP, args{"google.com", direct}, true},
|
||||
{"", router.HTTP, args{"google.com", proxy}, false},
|
||||
{"", router.HTTPS, args{"google.com", direct}, true},
|
||||
{"", router.HTTPS, args{"google.com", proxy}, false},
|
||||
{"", router.HTTP, args{"smtp.163.com", direct}, true},
|
||||
{"", router.HTTP, args{"smtp.163.com", proxy}, true},
|
||||
{"", router.HTTPS, args{"smtp.163.com", direct}, true},
|
||||
{"", router.HTTPS, args{"smtp.163.com", proxy}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.p.Ping(tt.args.domain, tt.args.dial); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Port.Ping() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (r *Router) localSite(domain string) bool {
|
||||
|
||||
ip := net.ParseIP(domain)
|
||||
if ip == nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ips, err := r.mmdb.Resolver.LookupIP(ctx, "ip", domain)
|
||||
if err != nil || len(ips) == 0 {
|
||||
log.Warn().Err(err).
|
||||
Str("domain", domain).
|
||||
Int("ips", len(ips)).
|
||||
Msg("resolve domain")
|
||||
return false
|
||||
}
|
||||
|
||||
ip = ips[0]
|
||||
}
|
||||
|
||||
for _, cidr := range r.mmdb.cidrs {
|
||||
if cidr.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if r.mmdb.Reader != nil {
|
||||
city, err := r.mmdb.City(ip)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).
|
||||
Str("domain", domain).
|
||||
IPAddr("ip", ip).
|
||||
Msg("mmdb search")
|
||||
return false
|
||||
}
|
||||
|
||||
if city.Country.IsoCode == "CN" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var pingClient = http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
func (r *Router) isAccess(domain string) bool {
|
||||
p := &ping{}
|
||||
r.cache.Remember(p, domain)
|
||||
return p.isAccess
|
||||
}
|
||||
|
||||
type ping struct {
|
||||
isAccess bool
|
||||
}
|
||||
|
||||
func (p *ping) Fulfill(key interface{}) error {
|
||||
_, err := http.Head(net.JoinHostPort(key.(string), "80"))
|
||||
p.isAccess = (err == nil)
|
||||
return nil
|
||||
}
|
||||
+88
-123
@@ -2,152 +2,117 @@ package router
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/miekg/dns"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/wweir/sower/pkg/dhcp"
|
||||
"github.com/wweir/sower/pkg/mem"
|
||||
"github.com/wweir/sower/util"
|
||||
"github.com/wweir/util-go/log"
|
||||
"github.com/wweir/util-go/mem"
|
||||
)
|
||||
|
||||
// Route implement a router for each request
|
||||
type Route struct {
|
||||
once sync.Once
|
||||
cache *mem.Cache
|
||||
type Router struct {
|
||||
blockRule *util.Node
|
||||
directRule *util.Node
|
||||
proxyRule *util.Node
|
||||
|
||||
ProxyAddress string
|
||||
password []byte
|
||||
ProxyDial func(network, host string, port uint16) (net.Conn, error)
|
||||
cache *mem.Cache
|
||||
|
||||
DetectLevel int // dynamic detect proxy level
|
||||
blockRule *util.Node
|
||||
proxyRule *util.Node
|
||||
directRule *util.Node
|
||||
PersistFn func(string)
|
||||
}
|
||||
dns struct {
|
||||
dns.Client
|
||||
connCh chan *dns.Conn
|
||||
}
|
||||
|
||||
func NewRoute(address, password string, detectLevel int,
|
||||
blocklist, proxylist, directlist []string, persist func(string)) *Route {
|
||||
return &Route{
|
||||
cache: mem.New(4 * time.Hour),
|
||||
ProxyAddress: address,
|
||||
password: []byte(password),
|
||||
mmdb struct {
|
||||
*geoip2.Reader
|
||||
*net.Resolver
|
||||
|
||||
DetectLevel: detectLevel,
|
||||
blockRule: util.NewNodeFromRules(blocklist...),
|
||||
proxyRule: util.NewNodeFromRules(proxylist...),
|
||||
directRule: util.NewNodeFromRules(directlist...),
|
||||
PersistFn: persist,
|
||||
cidrs []*net.IPNet
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldProxy check if the domain shoule request though proxy
|
||||
func (r *Route) GenProxyCheck(sync bool) func(string) (bool, bool) {
|
||||
detect := func(domain string) bool {
|
||||
go r.cache.Remember(r, domain)
|
||||
return true
|
||||
func NewRouter(proxyDial func(network, host string, port uint16) (net.Conn, error)) *Router {
|
||||
r := Router{
|
||||
blockRule: util.NewNodeFromRules(),
|
||||
directRule: util.NewNodeFromRules(),
|
||||
proxyRule: util.NewNodeFromRules("google.*"),
|
||||
ProxyDial: proxyDial,
|
||||
cache: mem.New(time.Hour), // TODO: config
|
||||
}
|
||||
if sync {
|
||||
detect = func(domain string) bool {
|
||||
r.cache.Remember(r, domain)
|
||||
if r.proxyRule.Match(domain) {
|
||||
return true
|
||||
|
||||
r.dns.connCh = make(chan *dns.Conn, 1)
|
||||
go r.dialDNSConn()
|
||||
|
||||
return &r
|
||||
}
|
||||
|
||||
func (r *Router) dialDNSConn() {
|
||||
for {
|
||||
server, err := dhcp.GetDNSServer()
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("ip", server).
|
||||
Msg("get upstream dns server")
|
||||
|
||||
for {
|
||||
conn, err := dns.DialTimeout("udp", net.JoinHostPort(server, "53"), time.Second)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("ip", server).Msg("dial dns server")
|
||||
break
|
||||
}
|
||||
return !r.directRule.Match(domain)
|
||||
}
|
||||
}
|
||||
|
||||
return func(domain string) (bool, bool) {
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
// break deadlook, for wildcard
|
||||
if sepCount := strings.Count(domain, "."); sepCount == 0 || sepCount >= 5 {
|
||||
return false, false
|
||||
r.dns.connCh <- conn
|
||||
}
|
||||
|
||||
if r.blockRule.Match(domain) {
|
||||
return true, false
|
||||
}
|
||||
|
||||
if r.proxyRule.Match(domain) {
|
||||
return false, true
|
||||
}
|
||||
if r.directRule.Match(domain) {
|
||||
return false, false
|
||||
}
|
||||
|
||||
return false, detect(domain)
|
||||
}
|
||||
}
|
||||
|
||||
// Get implement for cache
|
||||
func (r *Route) Get(key interface{}) (err error) {
|
||||
domain := key.(string)
|
||||
func (r *Router) RouteHandle(conn net.Conn, domain string, port uint16) error {
|
||||
addr := net.JoinHostPort(domain, strconv.FormatUint(uint64(port), 10))
|
||||
|
||||
httpScore, httpsScore := r.detect(domain)
|
||||
log.Infow("detect", "domain", domain, "http", httpScore, "https", httpsScore)
|
||||
switch {
|
||||
case r.blockRule.Match(domain):
|
||||
return nil
|
||||
|
||||
if httpScore+httpsScore >= r.DetectLevel {
|
||||
r.directRule.Add(domain)
|
||||
if r.PersistFn != nil {
|
||||
r.PersistFn(domain)
|
||||
}
|
||||
case r.directRule.Match(domain):
|
||||
return r.DirectHandle(conn, addr)
|
||||
|
||||
case r.proxyRule.Match(domain):
|
||||
return r.ProxyHandle(conn, domain, port)
|
||||
|
||||
case r.localSite(domain):
|
||||
return r.DirectHandle(conn, addr)
|
||||
case r.isAccess(domain):
|
||||
return r.DirectHandle(conn, addr)
|
||||
case port == 80:
|
||||
return r.DirectHandle(conn, addr)
|
||||
case port == 443:
|
||||
return r.DirectHandle(conn, addr)
|
||||
default:
|
||||
return r.ProxyHandle(conn, domain, port)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) ProxyHandle(conn net.Conn, domain string, port uint16) error {
|
||||
start := time.Now()
|
||||
rc, err := r.ProxyDial("tcp", domain, port)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "dial proxy to (%s:%d), spend (%s)", domain, port, time.Since(start))
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
util.Relay(conn, rc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// detect and caculate direct connection and proxy connection score
|
||||
func (r *Route) detect(domain string) (http, https int) {
|
||||
wg := sync.WaitGroup{}
|
||||
httpScore, httpsScore := new(int32), new(int32)
|
||||
for _, ping := range [...]struct {
|
||||
shouldProxy bool
|
||||
port Port
|
||||
}{
|
||||
{shouldProxy: true, port: HTTP},
|
||||
{shouldProxy: true, port: HTTPS},
|
||||
{shouldProxy: false, port: HTTP},
|
||||
{shouldProxy: false, port: HTTPS},
|
||||
} {
|
||||
wg.Add(1)
|
||||
go func(shouldProxy bool, port Port) {
|
||||
defer wg.Done()
|
||||
|
||||
if err := port.Ping(domain, func(domain string) (net.Conn, error) {
|
||||
return transport.Dial(domain,
|
||||
func(domain string) (proxyAddr string, password []byte) {
|
||||
if shouldProxy {
|
||||
return r.ProxyAddress, r.password
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
}); err != nil {
|
||||
log.Warnw("sower dial", "proxy", shouldProxy, "host", domain, "port", port, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case shouldProxy && port == HTTP:
|
||||
if !atomic.CompareAndSwapInt32(httpScore, 0, -2) {
|
||||
atomic.AddInt32(httpScore, -1)
|
||||
}
|
||||
case shouldProxy && port == HTTPS:
|
||||
if !atomic.CompareAndSwapInt32(httpsScore, 0, -2) {
|
||||
atomic.AddInt32(httpsScore, -1)
|
||||
}
|
||||
case !shouldProxy && port == HTTP:
|
||||
if !atomic.CompareAndSwapInt32(httpScore, 0, 2) {
|
||||
atomic.AddInt32(httpScore, 1)
|
||||
}
|
||||
case !shouldProxy && port == HTTPS:
|
||||
if !atomic.CompareAndSwapInt32(httpsScore, 0, 2) {
|
||||
atomic.AddInt32(httpsScore, 1)
|
||||
}
|
||||
}
|
||||
}(ping.shouldProxy, ping.port)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return int(*httpScore), int(*httpsScore)
|
||||
func (r *Router) DirectHandle(conn net.Conn, addr string) error {
|
||||
dur, err := util.RelayTo(conn, addr)
|
||||
return errors.Wrapf(err, "direct relay to (%s), spend (%s)", addr, dur)
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/wweir/sower/util"
|
||||
)
|
||||
|
||||
// checksum(>=0x80) + port + target_length + target + data
|
||||
// data(HTTP, first byte < 0x7F)
|
||||
type head struct {
|
||||
Checksum byte
|
||||
Port uint16
|
||||
AddrLen uint8
|
||||
}
|
||||
|
||||
func ParseProxyConn(conn net.Conn, password []byte) (net.Conn, string) {
|
||||
teeConn := &util.TeeConn{Conn: conn}
|
||||
defer teeConn.Stop()
|
||||
|
||||
h := &head{}
|
||||
if err := binary.Read(teeConn, binary.BigEndian, h); err != nil || h.Checksum < 0x80 {
|
||||
return teeConn, ""
|
||||
}
|
||||
|
||||
buf := make([]byte, int(h.AddrLen))
|
||||
if _, err := io.ReadFull(teeConn, buf); err != nil {
|
||||
return teeConn, ""
|
||||
}
|
||||
|
||||
if h.Checksum != sumChecksum(buf, password) {
|
||||
return teeConn, ""
|
||||
}
|
||||
|
||||
teeConn.Reset()
|
||||
return teeConn, net.JoinHostPort(string(buf), strconv.Itoa(int(h.Port)))
|
||||
}
|
||||
|
||||
func ToProxyConn(conn net.Conn, tgtHost string, tgtPort uint16, password []byte) (net.Conn, error) {
|
||||
h := &head{
|
||||
Checksum: sumChecksum([]byte(tgtHost), password),
|
||||
Port: tgtPort,
|
||||
AddrLen: uint8(len(tgtHost)),
|
||||
}
|
||||
if err := binary.Write(conn, binary.BigEndian, h); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data = []byte(tgtHost)
|
||||
var err error
|
||||
for n, nn := 0, 0; nn < len(data); nn += n {
|
||||
if n, err = conn.Write(data[nn:]); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func sumChecksum(target, password []byte) byte {
|
||||
return md5.Sum(append(target, password...))[0] | 0x80
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func IsSocks5Schema(addr string) (string, bool) {
|
||||
if strings.HasPrefix(addr, "socks5://") {
|
||||
return strings.TrimPrefix(addr, "socks5://"), true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(addr, "socks5h://") {
|
||||
return strings.TrimPrefix(addr, "socks5h://"), true
|
||||
}
|
||||
|
||||
return addr, false
|
||||
}
|
||||
|
||||
func ToSocks5(c net.Conn, host string, port uint16) (net.Conn, error) {
|
||||
return &conn{
|
||||
init: make(chan struct{}),
|
||||
Conn: c,
|
||||
domain: host,
|
||||
port: port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
init chan struct{}
|
||||
domain string
|
||||
port uint16
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *conn) Read(b []byte) (n int, err error) {
|
||||
select {
|
||||
case <-c.init:
|
||||
return c.Conn.Read(b)
|
||||
default:
|
||||
return 0, c.clientHandshake()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) Write(b []byte) (n int, err error) {
|
||||
select {
|
||||
case <-c.init:
|
||||
return c.Conn.Write(b)
|
||||
default:
|
||||
return 0, c.clientHandshake()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) clientHandshake() error {
|
||||
{
|
||||
req := &authReq{
|
||||
VER: 5,
|
||||
NMETHODS: 1,
|
||||
METHODS: [1]byte{0}, // NO AUTHENTICATION REQUIRED
|
||||
}
|
||||
if err := binary.Write(c.Conn, binary.BigEndian, req); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
}
|
||||
{
|
||||
resp := &authResp{}
|
||||
if err := binary.Read(c.Conn, binary.BigEndian, resp); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
}
|
||||
{
|
||||
reqHead := requestHead{
|
||||
VER: 5, // socks5
|
||||
CMD: 1, // CONNECT
|
||||
RSV: 0, // RESERVED
|
||||
ATYP: 3, // DOMAINNAME
|
||||
}
|
||||
|
||||
if err := binary.Write(c.Conn, binary.BigEndian, reqHead); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, 1 /*LEN*/ +len(c.domain)+2 /*PORT*/)
|
||||
buf = append(buf, byte(len(c.domain)))
|
||||
buf = append(buf, []byte(c.domain)...)
|
||||
buf = append(buf, byte(c.port>>8), byte(c.port))
|
||||
if _, err := c.Conn.Write(buf); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
}
|
||||
{
|
||||
head := replyHead{}
|
||||
if err := binary.Read(c.Conn, binary.BigEndian, &head); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
|
||||
switch head.REP {
|
||||
case 0x00:
|
||||
default:
|
||||
return xerrors.Errorf("socks5 handshake fail, return code: %d", head.REP)
|
||||
}
|
||||
|
||||
var bindAddr addrType
|
||||
switch head.ATYP {
|
||||
case 0x01: // IPv4
|
||||
bindAddr = addrTypeIPv4{}
|
||||
case 0x03: // domain name
|
||||
bindAddr = addrTypeDomain{}
|
||||
case 0x04: // IPv6
|
||||
bindAddr = addrTypeIPv6{}
|
||||
default:
|
||||
return xerrors.New("invalid connect type")
|
||||
}
|
||||
if err := bindAddr.Fullfill(c.Conn); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
close(c.init)
|
||||
return nil
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// https://tools.ietf.org/html/rfc1928
|
||||
|
||||
// 1. client send auth request
|
||||
type authReq struct {
|
||||
VER byte
|
||||
NMETHODS byte
|
||||
METHODS [1]byte // 1 to 255, fix to no authentication
|
||||
}
|
||||
|
||||
// 2. server response auth request
|
||||
type authResp struct {
|
||||
VER byte
|
||||
METHOD byte
|
||||
}
|
||||
|
||||
// 3. client request with target address
|
||||
type requestHead struct {
|
||||
VER byte
|
||||
CMD byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
}
|
||||
|
||||
// 4. server response with the address that assigned to connect to target address
|
||||
type replyHead struct {
|
||||
VER byte
|
||||
REP byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
}
|
||||
|
||||
type addrType interface {
|
||||
Fullfill(r io.Reader) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// ATYP:
|
||||
// 0x01 -> net.IPv4len
|
||||
// 0x03 -> first byte is length
|
||||
// 0x04 -> net.IPv6len
|
||||
type addrTypeIPv4 struct {
|
||||
DST_ADDR [4]byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
func (a addrTypeIPv4) Fullfill(r io.Reader) error {
|
||||
return binary.Read(r, binary.BigEndian, &a)
|
||||
}
|
||||
func (a addrTypeIPv4) String() string {
|
||||
return net.JoinHostPort(
|
||||
net.IP(a.DST_ADDR[:]).String(),
|
||||
strconv.FormatUint(uint64(a.DST_PORT), 10),
|
||||
)
|
||||
}
|
||||
|
||||
type addrTypeDomain struct {
|
||||
DST_ADDR_LEN uint8
|
||||
DST_ADDR []byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
func (a addrTypeDomain) Fullfill(r io.Reader) error {
|
||||
buf := make([]byte, 256)
|
||||
// domain length
|
||||
if _, err := io.ReadFull(r, buf[:1]); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
a.DST_ADDR_LEN = uint8(buf[0])
|
||||
|
||||
// domain
|
||||
if _, err := io.ReadFull(r, buf[:int(buf[0])]); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
a.DST_ADDR = buf[:int(buf[0])]
|
||||
|
||||
// port
|
||||
if _, err := io.ReadFull(r, buf[:2]); err != nil {
|
||||
return xerrors.New(err.Error())
|
||||
}
|
||||
a.DST_PORT = binary.BigEndian.Uint16(buf[:2])
|
||||
|
||||
return nil
|
||||
}
|
||||
func (a addrTypeDomain) String() string {
|
||||
return net.JoinHostPort(
|
||||
net.IP(a.DST_ADDR[:]).String(),
|
||||
strconv.FormatUint(uint64(a.DST_PORT), 10),
|
||||
)
|
||||
}
|
||||
|
||||
type addrTypeIPv6 struct {
|
||||
DST_ADDR [16]byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
func (a addrTypeIPv6) Fullfill(r io.Reader) error {
|
||||
return binary.Read(r, binary.BigEndian, &a)
|
||||
}
|
||||
func (a addrTypeIPv6) String() string {
|
||||
return net.JoinHostPort(
|
||||
net.IP(a.DST_ADDR[:]).String(),
|
||||
strconv.FormatUint(uint64(a.DST_PORT), 10),
|
||||
)
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func ParseSocks5(conn net.Conn) (tgtaddr string, err error) {
|
||||
{
|
||||
authReq := new(authReq)
|
||||
if err := binary.Read(conn, binary.BigEndian, authReq); err != nil {
|
||||
return "", xerrors.New(err.Error())
|
||||
}
|
||||
|
||||
if authReq.VER != 5 || // socks5
|
||||
authReq.NMETHODS != 1 ||
|
||||
authReq.METHODS[0] != 0 { // NO_AUTH
|
||||
return "", xerrors.New("invalid socks5 auth method")
|
||||
}
|
||||
}
|
||||
{
|
||||
if err := binary.Write(conn, binary.BigEndian, &authResp{
|
||||
VER: 5,
|
||||
METHOD: 1,
|
||||
}); err != nil {
|
||||
return "", xerrors.New(err.Error())
|
||||
}
|
||||
}
|
||||
{
|
||||
head := &requestHead{}
|
||||
if err := binary.Read(conn, binary.BigEndian, head); err != nil {
|
||||
return "", xerrors.New(err.Error())
|
||||
}
|
||||
if head.VER != 5 ||
|
||||
head.CMD != 1 {
|
||||
return "", xerrors.New("invalid socks5 connect request")
|
||||
}
|
||||
|
||||
var addr addrType
|
||||
switch head.ATYP {
|
||||
case 0x01: // IPv4
|
||||
addr = addrTypeIPv4{}
|
||||
case 0x03: // domain name
|
||||
addr = addrTypeDomain{}
|
||||
case 0x04: // IPv6
|
||||
addr = addrTypeIPv6{}
|
||||
default:
|
||||
return "", xerrors.New("invalid connect type")
|
||||
}
|
||||
if err := addr.Fullfill(conn); err != nil {
|
||||
return "", xerrors.New(err.Error())
|
||||
}
|
||||
tgtaddr = addr.String()
|
||||
}
|
||||
{
|
||||
if err := binary.Write(conn, binary.BigEndian, &replyHead{
|
||||
VER: 5,
|
||||
REP: 0,
|
||||
RSV: 0,
|
||||
ATYP: 1,
|
||||
}); err != nil {
|
||||
return "", xerrors.New(err.Error())
|
||||
}
|
||||
|
||||
// FIXME: return the real address
|
||||
if _, err := conn.Write(make([]byte, 6)); err != nil {
|
||||
return "", xerrors.New(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return tgtaddr, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package sower
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/wweir/sower/pkg/teeconn"
|
||||
)
|
||||
|
||||
// https://en.wikipedia.org/wiki/Domain_Name_System
|
||||
const maxDomainLength = 253
|
||||
|
||||
var headSize = binary.Size(new(Head))
|
||||
|
||||
// action(>=0x80) + checksum + port + target + data
|
||||
// data(HTTP, first byte < 0x7F)
|
||||
type Head struct {
|
||||
Cmd byte
|
||||
Checksum byte
|
||||
Port uint16
|
||||
TgtAddr [maxDomainLength]byte
|
||||
}
|
||||
|
||||
func (h *Head) Network() string { return "tcp" }
|
||||
func (h *Head) String() string {
|
||||
idx := bytes.IndexRune(h.TgtAddr[:], 0)
|
||||
addr := string(h.TgtAddr[:idx])
|
||||
return net.JoinHostPort(addr, strconv.Itoa(int(h.Port)))
|
||||
}
|
||||
|
||||
type Sower struct {
|
||||
password []byte
|
||||
}
|
||||
|
||||
func New(password string) *Sower {
|
||||
return &Sower{
|
||||
password: []byte(password),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sower) Unwrap(conn *teeconn.Conn) net.Addr {
|
||||
buf := make([]byte, headSize)
|
||||
if n, err := conn.Read(buf); err != nil || n != headSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
h := &Head{}
|
||||
binary.Read(bytes.NewReader(buf), binary.BigEndian, h)
|
||||
switch h.Cmd {
|
||||
case 0x80:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if h.Checksum != sumChecksum(h.TgtAddr, s.password) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *Sower) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
tgtAddr := [maxDomainLength]byte{}
|
||||
copy(tgtAddr[:len(tgtHost)], []byte(tgtHost))
|
||||
|
||||
return binary.Write(conn, binary.BigEndian, &Head{
|
||||
Cmd: 0x80,
|
||||
Checksum: sumChecksum(tgtAddr, s.password),
|
||||
Port: tgtPort,
|
||||
TgtAddr: tgtAddr,
|
||||
})
|
||||
}
|
||||
|
||||
func sumChecksum(target [maxDomainLength]byte, password []byte) byte {
|
||||
return md5.Sum(append(target[:], password...))[0]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/wweir/sower/pkg/teeconn"
|
||||
)
|
||||
|
||||
type Transport interface {
|
||||
Unwrap(conn *teeconn.Conn) net.Addr
|
||||
Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/wweir/sower/pkg/teeconn"
|
||||
"github.com/wweir/sower/transport/sower"
|
||||
"github.com/wweir/sower/transport/trojan"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.Logger = log.Logger.With().Caller().Logger()
|
||||
}
|
||||
|
||||
func testPipe(tran Transport) net.Addr {
|
||||
r, w := net.Pipe()
|
||||
defer r.Close()
|
||||
|
||||
go func(w net.Conn) {
|
||||
defer w.Close()
|
||||
tran.Wrap(w, "sower", 443)
|
||||
}(w)
|
||||
|
||||
return tran.Unwrap(teeconn.New(r))
|
||||
}
|
||||
|
||||
func Test_Transports(t *testing.T) {
|
||||
if addr := testPipe(newSower()); addr == nil || strings.TrimSpace(addr.String()) != "sower:443" {
|
||||
t.Errorf("test sower, unexpected address: %s", addr)
|
||||
}
|
||||
|
||||
if addr := testPipe(newTrojan()); addr == nil || strings.TrimSpace(addr.String()) != "sower:443" {
|
||||
t.Errorf("test trojan, unexpected address: %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
func newSower() *sower.Sower {
|
||||
return sower.New("123")
|
||||
}
|
||||
|
||||
func newTrojan() *trojan.Trojan {
|
||||
return trojan.New("123")
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/wweir/sower/util"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// | hex(SHA224(password)) | CRLF | Trojan Request | CRLF | Payload |
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// | 56 | X'0D0A' | Variable | X'0D0A' | Variable |
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// +-----+------+----------+----------+
|
||||
// | CMD | ATYP | DST.ADDR | DST.PORT |
|
||||
// +-----+------+----------+----------+
|
||||
// | 1 | 1 | Variable | 2 |
|
||||
// +-----+------+----------+----------+
|
||||
// o CMD
|
||||
// o CONNECT X'01'
|
||||
// o UDP :X'03'
|
||||
// o ATYP
|
||||
// o IP V4 : X'01'
|
||||
// o domain: X'03'
|
||||
// o IP V6 : X'04'
|
||||
|
||||
type Trojan struct {
|
||||
Password [56]byte
|
||||
TrojanRequest
|
||||
}
|
||||
type TrojanRequest struct {
|
||||
CMD uint8
|
||||
ATYP uint8
|
||||
DstAddr []byte
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
func (t *TrojanRequest) Addr() string {
|
||||
switch t.ATYP {
|
||||
case 0x01:
|
||||
fallthrough
|
||||
case 0x04:
|
||||
return net.JoinHostPort(net.IP(t.DstAddr).String(), strconv.Itoa(int(t.DstPort)))
|
||||
case 0x03:
|
||||
return string(t.DstAddr) + ":" + strconv.Itoa(int(t.DstPort))
|
||||
default:
|
||||
panic("invalid ATYP")
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTrojanConn(conn net.Conn, password []byte) (net.Conn, *Trojan) {
|
||||
passData := sha256.Sum224(password)
|
||||
passHead := hex.EncodeToString(passData[:])
|
||||
|
||||
teeConn := &util.TeeConn{Conn: conn}
|
||||
defer teeConn.Stop()
|
||||
|
||||
buf := make([]byte, 56+2+1+1+1)
|
||||
if _, err := io.ReadFull(teeConn, buf); err != nil {
|
||||
return teeConn, nil
|
||||
}
|
||||
if string(buf[:56]) != passHead {
|
||||
return teeConn, nil
|
||||
}
|
||||
|
||||
t := Trojan{}
|
||||
t.CMD, t.ATYP = buf[58], buf[59]
|
||||
addrLen := buf[60]
|
||||
switch t.ATYP {
|
||||
case 0x01: //ipv4
|
||||
buf = make([]byte, net.IPv4len+2+2)
|
||||
buf[0] = addrLen
|
||||
if _, err := io.ReadFull(teeConn, buf[1:]); err != nil {
|
||||
return teeConn, nil
|
||||
}
|
||||
|
||||
case 0x04: //ipv6
|
||||
buf = make([]byte, net.IPv6len+2+2)
|
||||
buf[0] = addrLen
|
||||
if _, err := io.ReadFull(teeConn, buf[1:]); err != nil {
|
||||
return teeConn, nil
|
||||
}
|
||||
|
||||
case 0x03: // domain
|
||||
buf = make([]byte, addrLen+2+2)
|
||||
buf[0] = addrLen
|
||||
if _, err := io.ReadFull(teeConn, buf); err != nil {
|
||||
return teeConn, nil
|
||||
}
|
||||
default:
|
||||
return teeConn, nil
|
||||
}
|
||||
|
||||
t.DstAddr = buf[:len(buf)-4]
|
||||
t.DstPort = uint16(buf[len(buf)-4])<<8 + uint16(buf[len(buf)-3])
|
||||
|
||||
teeConn.Reset()
|
||||
return teeConn, &t
|
||||
}
|
||||
|
||||
func ToTrojanConn(conn net.Conn, tgtHost string, tgtPort uint16, password []byte) (net.Conn, error) {
|
||||
passData := sha256.Sum224(password)
|
||||
passHead := hex.EncodeToString(passData[:])
|
||||
|
||||
buf := make([]byte, 0, 56+2+1+1+1+len(tgtHost)+2+2)
|
||||
buf = append(buf, []byte(passHead)...)
|
||||
buf = append(buf, '\r', '\n')
|
||||
buf = append(buf, 1, 3, uint8(len(tgtHost)))
|
||||
buf = append(buf, []byte(tgtHost)...)
|
||||
buf = append(buf, byte(tgtPort>>8), byte(tgtPort))
|
||||
buf = append(buf, '\r', '\n')
|
||||
|
||||
if n, err := conn.Write(buf); err != nil || n != len(buf) {
|
||||
return conn, xerrors.Errorf("n: %d, msg: %s", n, err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package trojan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wweir/sower/pkg/teeconn"
|
||||
)
|
||||
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// | hex(SHA224(password)) | CRLF | Trojan Request | CRLF | Payload |
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// | 56 | X'0D0A' | Variable | X'0D0A' | Variable |
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// +-----+------+----------+----------+
|
||||
// | CMD | ATYP | DST.ADDR | DST.PORT |
|
||||
// +-----+------+----------+----------+
|
||||
// | 1 | 1 | Variable | 2 |
|
||||
// +-----+------+----------+----------+
|
||||
// o CMD
|
||||
// o CONNECT X'01'
|
||||
// o UDP :X'03'
|
||||
// o ATYP
|
||||
// o IP V4 : X'01'
|
||||
// o domain: X'03'
|
||||
// o IP V6 : X'04'
|
||||
|
||||
const headLen = 56 + 2 + 1 + 1
|
||||
|
||||
type staticHead struct {
|
||||
Passwd [56]byte
|
||||
CRLF [2]byte
|
||||
CMD uint8
|
||||
ATYP uint8
|
||||
}
|
||||
|
||||
type ipv4Addr struct {
|
||||
ADDR [4]byte
|
||||
PORT uint16
|
||||
CRLF [2]byte
|
||||
}
|
||||
|
||||
func (*ipv4Addr) Network() string { return "tcp" }
|
||||
func (a *ipv4Addr) String() string {
|
||||
return net.JoinHostPort(net.IP(a.ADDR[:]).String(), strconv.Itoa(int(a.PORT)))
|
||||
}
|
||||
|
||||
type ipv6Addr struct {
|
||||
ADDR [16]byte
|
||||
PORT uint16
|
||||
CRLF [2]byte
|
||||
}
|
||||
|
||||
func (*ipv6Addr) Network() string { return "tcp" }
|
||||
func (a *ipv6Addr) String() string {
|
||||
return net.JoinHostPort(net.IP(a.ADDR[:]).String(), strconv.Itoa(int(a.PORT)))
|
||||
}
|
||||
|
||||
type domain struct {
|
||||
ADDR string
|
||||
PORT uint16
|
||||
CRLF [2]byte
|
||||
}
|
||||
|
||||
func (*domain) Network() string { return "tcp" }
|
||||
func (a *domain) String() string {
|
||||
return net.JoinHostPort(a.ADDR, strconv.Itoa(int(a.PORT)))
|
||||
}
|
||||
func (a *domain) Fulfill(r io.Reader) error {
|
||||
buf := make([]byte, 1)
|
||||
if n, err := r.Read(buf); err != nil || n != 1 {
|
||||
return errors.New("read domain length failed")
|
||||
}
|
||||
|
||||
addrLen := int(buf[0])
|
||||
buf = make([]byte, addrLen+4)
|
||||
if n, err := r.Read(buf); err != nil || n != addrLen+4 {
|
||||
return errors.Wrap(err, "read doamin failed")
|
||||
}
|
||||
|
||||
a.ADDR = string(buf[:addrLen])
|
||||
a.PORT = uint16(buf[addrLen])<<8 + uint16(buf[addrLen+1])
|
||||
return nil
|
||||
}
|
||||
|
||||
type Trojan struct {
|
||||
headPasswd []byte
|
||||
|
||||
headIPv4 []byte
|
||||
headIPv6 []byte
|
||||
headDomain []byte
|
||||
}
|
||||
|
||||
func New(password string) *Trojan {
|
||||
t := &Trojan{
|
||||
headPasswd: make([]byte, 56),
|
||||
}
|
||||
passSum := sha256.Sum224([]byte(password))
|
||||
hex.Encode(t.headPasswd, passSum[:])
|
||||
|
||||
t.headIPv4 = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x01)
|
||||
t.headIPv6 = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x04)
|
||||
t.headDomain = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x03)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Trojan) Unwrap(conn *teeconn.Conn) net.Addr {
|
||||
buf := make([]byte, headLen)
|
||||
// do not use io.ReadFull to avoid hang
|
||||
if n, err := conn.Read(buf); err != nil || n != headLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
head := &staticHead{}
|
||||
if err := binary.Read(bytes.NewBuffer(buf), binary.BigEndian, head); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !bytes.Equal(head.Passwd[:], []byte(t.headPasswd)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
head.CMD, head.ATYP = buf[58], buf[59]
|
||||
switch head.ATYP {
|
||||
case 0x01: //ipv4
|
||||
addr := &ipv4Addr{}
|
||||
if err := binary.Read(conn, binary.BigEndian, addr); err != nil {
|
||||
return nil
|
||||
}
|
||||
return addr
|
||||
|
||||
case 0x04: //ipv6
|
||||
addr := &ipv6Addr{}
|
||||
if err := binary.Read(conn, binary.BigEndian, addr); err != nil {
|
||||
return nil
|
||||
}
|
||||
return addr
|
||||
|
||||
case 0x03: // domain
|
||||
addr := &domain{}
|
||||
if err := addr.Fulfill(conn); err != nil {
|
||||
return nil
|
||||
}
|
||||
return addr
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Trojan) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
var buf []byte
|
||||
ip := net.ParseIP(tgtHost)
|
||||
|
||||
switch {
|
||||
case len(ip.To4()) != 0:
|
||||
buf = make([]byte, headLen+net.IPv4len+4)
|
||||
buf = append(t.headIPv4, []byte(ip.To4())...)
|
||||
|
||||
case len(ip) != 0:
|
||||
buf = make([]byte, headLen+net.IPv6len+4)
|
||||
buf = append(t.headIPv6, []byte(ip)...)
|
||||
|
||||
default:
|
||||
buf = make([]byte, headLen+1+len(tgtHost)+4)
|
||||
buf = append(t.headDomain, byte(len(tgtHost)))
|
||||
buf = append(buf, []byte(tgtHost)...)
|
||||
}
|
||||
|
||||
buf = append(buf, byte(tgtPort>>8), byte(tgtPort), 0x0D, 0x0A)
|
||||
|
||||
if n, err := conn.Write(buf); err != nil || n != len(buf) {
|
||||
return errors.Errorf("n: %d, msg: %s", n, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/wweir/util-go/log"
|
||||
)
|
||||
|
||||
var (
|
||||
persistDNS string
|
||||
dnsAddr string
|
||||
resolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, dnsAddr)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func SetDNS(err error, dnsIP string) {
|
||||
if dnsIP != "" {
|
||||
persistDNS = dnsIP
|
||||
dnsAddr = net.JoinHostPort(dnsIP, "53")
|
||||
return
|
||||
} else if persistDNS != "" {
|
||||
return
|
||||
}
|
||||
|
||||
if e, ok := err.(*net.DNSError); !ok /*nil*/ || !e.IsNotFound {
|
||||
if dnsIP, err = dhcp.GetDefaultDNSServer(); err != nil {
|
||||
dnsIP, err = dhcp.GetDefaultDNSServer() // retry
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorw("get dns via dhcp", "err", err, "current_dns", dnsAddr)
|
||||
} else {
|
||||
dnsAddr = net.JoinHostPort(dnsIP, "53")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dial dial targetAddr with possiable proxy address
|
||||
func Dial(targetAddr string, dialAddr func(domain string) (proxyAddr string, password []byte)) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(targetAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
address, password := dialAddr(host)
|
||||
if address == "" {
|
||||
ips, err := resolver.LookupIPAddr(context.Background(), host)
|
||||
if err != nil { //retry
|
||||
ips, err = resolver.LookupIPAddr(context.Background(), host)
|
||||
}
|
||||
if err != nil {
|
||||
SetDNS(err, "")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return net.Dial("tcp", net.JoinHostPort(ips[0].String(), port))
|
||||
}
|
||||
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if addr, ok := IsSocks5Schema(address); ok {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conn, err = ToSocks5(conn, host, uint16(p)); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Resolver: resolver},
|
||||
"tcp", net.JoinHostPort(address, "443"), &tls.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ToTrojanConn(conn, host, uint16(p), password)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
type TeeConn struct {
|
||||
net.Conn
|
||||
buf []byte
|
||||
offset int
|
||||
stop bool // read
|
||||
EnableWrite bool
|
||||
}
|
||||
|
||||
func (t *TeeConn) Reread() {
|
||||
t.offset = 0
|
||||
}
|
||||
func (t *TeeConn) Reset() {
|
||||
t.buf = []byte{}
|
||||
t.offset = 0
|
||||
}
|
||||
func (t *TeeConn) Stop() {
|
||||
t.offset = 0
|
||||
t.stop = true
|
||||
}
|
||||
|
||||
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.stop {
|
||||
t.buf = append(t.buf, b[:n]...)
|
||||
t.offset += n
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (t *TeeConn) Write(b []byte) (n int, err error) {
|
||||
if t.stop || t.EnableWrite {
|
||||
return t.Conn.Write(b)
|
||||
}
|
||||
|
||||
return 0, io.EOF
|
||||
}
|
||||
+45
-6
@@ -1,11 +1,50 @@
|
||||
package util
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
func WithDefaultPort(addr string, port string) (address, host string) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return net.JoinHostPort(addr, port), addr
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func RelayTo(conn net.Conn, addr string) (time.Duration, error) {
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
addr = net.JoinHostPort(addr, "80")
|
||||
}
|
||||
return addr, host
|
||||
|
||||
start := time.Now()
|
||||
rc, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return time.Since(start), errors.Wrapf(err, "dial %s", addr)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
Relay(conn, rc)
|
||||
return time.Since(start), nil
|
||||
}
|
||||
|
||||
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, io.TeeReader(src, os.Stdout))
|
||||
io.Copy(dst, src)
|
||||
|
||||
if atomic.CompareAndSwapInt32(exitFlag, 0, 1) {
|
||||
// wakeup blocked goroutine
|
||||
now := time.Now()
|
||||
src.SetDeadline(now)
|
||||
dst.SetDeadline(now)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user