mirror of
https://github.com/stefan01/transocks.git
synced 2025-02-21 03:00:48 +07:00
Reimplement transocks based on cybozu-go/cmd .
This commit is contained in:
parent
a654def39b
commit
b44c8b4e63
@ -1,8 +1,14 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- go get github.com/golang/lint/golint
|
||||
|
||||
script:
|
||||
- go install ./...
|
||||
- go test -v ./...
|
||||
- go vet -x ./...
|
||||
- $HOME/gopath/bin/golint -set_exit_status -min_confidence 0.81 ./...
|
||||
|
16
CHANGELOG.md
Normal file
16
CHANGELOG.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- transocks now adopts [github.com/cybozu-go/cmd][cmd] framework.
|
||||
As a result, it implements [the common spec][spec] including graceful restart.
|
||||
|
||||
### Changed
|
||||
- The default configuration file path is now `/etc/transocks.toml`.
|
||||
- Configuration items for logging is changed.
|
||||
|
||||
[cmd]: https://github.com/cybozu-go/cmd
|
||||
[spec]: https://github.com/cybozu-go/cmd/blob/master/README.md#specifications
|
||||
[Unreleased]: https://github.com/cybozu-go/transocks/compare/v0.1...HEAD
|
50
README.md
50
README.md
@ -1,5 +1,8 @@
|
||||
[][godoc]
|
||||
[](https://travis-ci.org/cybozu-go/transocks)
|
||||
[][releases]
|
||||
[][godoc]
|
||||
[](https://travis-ci.org/cybozu-go/transocks)
|
||||
[](https://goreportcard.com/report/github.com/cybozu-go/transocks)
|
||||
[](LICENSE)
|
||||
|
||||
transocks - a transparent SOCKS5/HTTP proxy
|
||||
===========================================
|
||||
@ -21,35 +24,42 @@ Features
|
||||
* SOCKS5 and HTTP proxy (CONNECT)
|
||||
|
||||
We recommend using SOCKS5 server if available.
|
||||
Looking for a good SOCKS5 server? Take a look at our [usocksd][]!
|
||||
Take a look at our SOCKS server [usocksd][] if you are looking for.
|
||||
|
||||
HTTP proxies often prohibits CONNECT method to make connections
|
||||
to ports other than 443. Make sure your HTTP proxy allows CONNECT
|
||||
to the ports you want.
|
||||
|
||||
* Graceful stop & restart
|
||||
|
||||
* On SIGINT/SIGTERM, transocks stops gracefully.
|
||||
* On SIGHUP, transocks restarts gracefully.
|
||||
|
||||
* Library and executable
|
||||
|
||||
transocks comes with a handy executable.
|
||||
You may use the library to create your own.
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
Use Go 1.7 or better.
|
||||
|
||||
```
|
||||
go get -u github.com/cybozu-go/transocks/...
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
`transocks [-h] [-f CONFIG]`
|
||||
|
||||
The default configuration file path is `/usr/local/etc/transocks.toml`.
|
||||
The default configuration file path is `/etc/transocks.toml`.
|
||||
|
||||
`transocks` does not have *daemon* mode. Use systemd or upstart to
|
||||
run it on your background.
|
||||
In addition, transocks implements [the common spec](https://github.com/cybozu-go/cmd#specifications) from [`cybozu-go/cmd`](https://github.com/cybozu-go/cmd).
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
Use Go 1.5 or better.
|
||||
|
||||
```
|
||||
go get github.com/cybozu-go/transocks/cmd/transocks
|
||||
```
|
||||
transocks does not have *daemon* mode. Use systemd to run it
|
||||
on your background.
|
||||
|
||||
Configuration file format
|
||||
-------------------------
|
||||
@ -66,8 +76,10 @@ listen = "localhost:1081"
|
||||
proxy_url = "socks5://10.20.30.40:1080" # for SOCKS5 server
|
||||
#proxy_url = "http://10.20.30.40:3128" # for HTTP proxy server
|
||||
|
||||
log_level = "info"
|
||||
log_file = "/var/log/transocks.log"
|
||||
[log]
|
||||
filename = "/path/to/file" # default to stderr
|
||||
level = "info" # critical", error, warning, info, debug
|
||||
format = "json" # plain, logfmt, json
|
||||
```
|
||||
|
||||
Redirecting connections by iptables
|
||||
@ -110,13 +122,7 @@ License
|
||||
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
[@ymmt2005][]
|
||||
|
||||
[godoc]: https://godoc.org/github.com/cybozu-go/transocks
|
||||
[Squid]: http://www.squid-cache.org/
|
||||
[usocksd]: https://github.com/cybozu-go/usocksd
|
||||
[TOML]: https://github.com/toml-lang/toml
|
||||
[@ymmt2005]: https://github.com/ymmt2005
|
||||
|
@ -1,79 +1,90 @@
|
||||
// transocks server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/cybozu-go/cmd"
|
||||
"github.com/cybozu-go/log"
|
||||
"github.com/cybozu-go/transocks"
|
||||
)
|
||||
|
||||
type tomlConfig struct {
|
||||
Listen string
|
||||
ProxyURL string `toml:"proxy_url"`
|
||||
LogLevel string `toml:"log_level"`
|
||||
LogFile string `toml:"log_file"`
|
||||
Listen string `toml:"listen"`
|
||||
ProxyURL string `toml:"proxy_url"`
|
||||
Log cmd.LogConfig `toml:"log"`
|
||||
}
|
||||
|
||||
var (
|
||||
configFile = flag.String("f", "/usr/local/etc/transocks.toml",
|
||||
configFile = flag.String("f", "/etc/transocks.toml",
|
||||
"TOML configuration file path")
|
||||
)
|
||||
|
||||
func loadConfig() (*transocks.Config, string, error) {
|
||||
func loadConfig() (*transocks.Config, error) {
|
||||
tc := new(tomlConfig)
|
||||
md, err := toml.DecodeFile(*configFile, tc)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
if len(md.Undecoded()) > 0 {
|
||||
return nil, "", fmt.Errorf("undecoded key in TOML: %v", md.Undecoded())
|
||||
return nil, fmt.Errorf("undecoded key in TOML: %v", md.Undecoded())
|
||||
}
|
||||
|
||||
c := transocks.NewConfig()
|
||||
c.Listen = tc.Listen
|
||||
c.Addr = tc.Listen
|
||||
|
||||
u, err := url.Parse(tc.ProxyURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
c.ProxyURL = u
|
||||
|
||||
if err = log.DefaultLogger().SetThresholdByName(tc.LogLevel); err != nil {
|
||||
return nil, "", err
|
||||
err = tc.Log.Apply()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, tc.LogFile, nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func serve(lns []net.Listener, c *transocks.Config) {
|
||||
s, err := transocks.NewServer(c)
|
||||
if err != nil {
|
||||
log.ErrorExit(err)
|
||||
}
|
||||
|
||||
for _, ln := range lns {
|
||||
s.Serve(ln)
|
||||
}
|
||||
err = cmd.Wait()
|
||||
if err != nil && !cmd.IsSignaled(err) {
|
||||
log.ErrorExit(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
c, logfile, err := loadConfig()
|
||||
c, err := loadConfig()
|
||||
if err != nil {
|
||||
log.ErrorExit(err)
|
||||
}
|
||||
|
||||
if len(logfile) > 0 {
|
||||
f, err := os.OpenFile(logfile, os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
log.ErrorExit(err)
|
||||
}
|
||||
defer f.Close()
|
||||
log.DefaultLogger().SetOutput(f)
|
||||
g := &cmd.Graceful{
|
||||
Listen: func() ([]net.Listener, error) {
|
||||
return transocks.Listeners(c)
|
||||
},
|
||||
Serve: func(lns []net.Listener) {
|
||||
serve(lns, c)
|
||||
},
|
||||
}
|
||||
g.Run()
|
||||
|
||||
srv, err := transocks.NewServer(c)
|
||||
if err != nil {
|
||||
err = cmd.Wait()
|
||||
if err != nil && !cmd.IsSignaled(err) {
|
||||
log.ErrorExit(err)
|
||||
}
|
||||
log.Info("server starts", nil)
|
||||
|
||||
srv.Serve()
|
||||
|
||||
log.Info("server ends", nil)
|
||||
}
|
||||
|
46
config.go
46
config.go
@ -5,18 +5,32 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cybozu-go/cmd"
|
||||
"github.com/cybozu-go/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// NAT mode
|
||||
ModeNAT = "nat"
|
||||
defaultShutdownTimeout = 1 * time.Minute
|
||||
)
|
||||
|
||||
// Mode is the type of transocks mode.
|
||||
type Mode string
|
||||
|
||||
func (m Mode) String() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
const (
|
||||
// ModeNAT is mode constant for NAT.
|
||||
ModeNAT = Mode("nat")
|
||||
)
|
||||
|
||||
// Config keeps configurations for Server.
|
||||
type Config struct {
|
||||
// Listen is the listening address.
|
||||
// e.g. "localhost:1081"
|
||||
Listen string
|
||||
// Addr is the listening address.
|
||||
Addr string
|
||||
|
||||
// ProxyURL is the URL for upstream proxy.
|
||||
//
|
||||
@ -27,27 +41,39 @@ type Config struct {
|
||||
ProxyURL *url.URL
|
||||
|
||||
// Mode determines how clients are routed to transocks.
|
||||
// Default is "nat". No other options are available at this point.
|
||||
Mode string
|
||||
// Default is ModeNAT. No other options are available at this point.
|
||||
Mode Mode
|
||||
|
||||
// ShutdownTimeout is the maximum duration the server waits for
|
||||
// all connections to be closed before shutdown.
|
||||
//
|
||||
// Zero duration disables timeout. Default is 1 minute.
|
||||
ShutdownTimeout time.Duration
|
||||
|
||||
// Dialer is the base dialer to connect to the proxy server.
|
||||
// The server uses the default dialer if this is nil.
|
||||
Dialer *net.Dialer
|
||||
|
||||
// Logger can be used to provide a custom logger.
|
||||
// If nil, the default logger is used.
|
||||
Logger *log.Logger
|
||||
|
||||
// Env can be used to specify a cmd.Environment on which the server runs.
|
||||
// If nil, the server will run on the global environment.
|
||||
Env *cmd.Environment
|
||||
}
|
||||
|
||||
// NewConfig creates and initializes a new Config.
|
||||
func NewConfig() *Config {
|
||||
c := new(Config)
|
||||
c.Mode = ModeNAT
|
||||
c.ShutdownTimeout = defaultShutdownTimeout
|
||||
return c
|
||||
}
|
||||
|
||||
// validate validates the configuration.
|
||||
// It returns non-nil error if the configuration is not valid.
|
||||
func (c *Config) validate() error {
|
||||
if len(c.Listen) == 0 {
|
||||
return errors.New("Listen is empty")
|
||||
}
|
||||
if c.ProxyURL == nil {
|
||||
return errors.New("ProxyURL is nil")
|
||||
}
|
||||
|
@ -10,7 +10,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SO_ORIGINAL_DST = 80
|
||||
// SO_ORIGINAL_DST is a Linux getsockopt optname.
|
||||
SO_ORIGINAL_DST = 80
|
||||
|
||||
// IP6T_SO_ORIGINAL_DST a Linux getsockopt optname.
|
||||
IP6T_SO_ORIGINAL_DST = 80
|
||||
)
|
||||
|
||||
|
@ -22,10 +22,10 @@ func TestGetOriginalDST(t *testing.T) {
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
orig_addr, err := GetOriginalDST(c.(*net.TCPConn))
|
||||
origAddr, err := GetOriginalDST(c.(*net.TCPConn))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log(orig_addr.String())
|
||||
t.Log(origAddr.String())
|
||||
}
|
||||
|
194
server.go
194
server.go
@ -1,26 +1,39 @@
|
||||
package transocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cybozu-go/cmd"
|
||||
"github.com/cybozu-go/log"
|
||||
"github.com/cybozu-go/netutil"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDialer = &net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}
|
||||
const (
|
||||
keepAliveTimeout = 3 * time.Minute
|
||||
copyBufferSize = 64 << 10
|
||||
)
|
||||
|
||||
// Listeners returns a list of net.Listener.
|
||||
func Listeners(c *Config) ([]net.Listener, error) {
|
||||
ln, err := net.Listen("tcp", c.Addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []net.Listener{ln}, nil
|
||||
}
|
||||
|
||||
// Server provides transparent proxy server functions.
|
||||
type Server struct {
|
||||
config *Config
|
||||
dialer proxy.Dialer
|
||||
listener net.Listener
|
||||
cmd.Server
|
||||
mode Mode
|
||||
logger *log.Logger
|
||||
dialer proxy.Dialer
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
// NewServer creates Server.
|
||||
@ -30,99 +43,110 @@ func NewServer(c *Config) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := defaultDialer
|
||||
if c.Dialer != nil {
|
||||
dialer = c.Dialer
|
||||
dialer := c.Dialer
|
||||
if dialer == nil {
|
||||
dialer = &net.Dialer{
|
||||
KeepAlive: keepAliveTimeout,
|
||||
DualStack: true,
|
||||
}
|
||||
}
|
||||
proxy_dialer, err := proxy.FromURL(c.ProxyURL, dialer)
|
||||
pdialer, err := proxy.FromURL(c.ProxyURL, dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", c.Listen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger := c.Logger
|
||||
if logger == nil {
|
||||
logger = log.DefaultLogger()
|
||||
}
|
||||
|
||||
return &Server{c, proxy_dialer, l}, nil
|
||||
s := &Server{
|
||||
Server: cmd.Server{
|
||||
ShutdownTimeout: c.ShutdownTimeout,
|
||||
Env: c.Env,
|
||||
},
|
||||
mode: c.Mode,
|
||||
logger: logger,
|
||||
dialer: pdialer,
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, copyBufferSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
s.Server.Handler = s.handleConnection
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Serve accepts and handles new connections forever.
|
||||
func (s *Server) Serve() error {
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
log.Critical(err.Error(), nil)
|
||||
return err
|
||||
}
|
||||
tcp_conn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
conn.Close()
|
||||
panic("not a TCPConn!")
|
||||
}
|
||||
go s.handleConnection(tcp_conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleConnection(c *net.TCPConn) {
|
||||
defer c.Close()
|
||||
|
||||
var addr string
|
||||
|
||||
switch s.config.Mode {
|
||||
case ModeNAT:
|
||||
orig_addr, err := GetOriginalDST(c)
|
||||
if err != nil {
|
||||
log.Error(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
addr = orig_addr.String()
|
||||
default:
|
||||
addr = c.LocalAddr().String()
|
||||
}
|
||||
|
||||
if log.Enabled(log.LvDebug) {
|
||||
log.Debug("making proxy connection", map[string]interface{}{
|
||||
"_dst": addr,
|
||||
})
|
||||
}
|
||||
|
||||
pconn, err := s.dialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
log.Error(err.Error(), map[string]interface{}{
|
||||
"_dst": addr,
|
||||
func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
|
||||
tc, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
s.logger.Error("non-TCP connection", map[string]interface{}{
|
||||
"conn": conn,
|
||||
})
|
||||
return
|
||||
}
|
||||
defer pconn.Close()
|
||||
|
||||
ch := make(chan error, 2)
|
||||
go copyData(c, pconn, ch)
|
||||
go copyData(pconn, c, ch)
|
||||
for i := 0; i < 2; i++ {
|
||||
e := <-ch
|
||||
if e != nil {
|
||||
log.Error(e.Error(), map[string]interface{}{
|
||||
"_dst": addr,
|
||||
})
|
||||
break
|
||||
fields := cmd.FieldsFromContext(ctx)
|
||||
fields[log.FnType] = "access"
|
||||
fields["client_addr"] = conn.RemoteAddr().String()
|
||||
|
||||
var addr string
|
||||
switch s.mode {
|
||||
case ModeNAT:
|
||||
origAddr, err := GetOriginalDST(tc)
|
||||
if err != nil {
|
||||
fields[log.FnError] = err.Error()
|
||||
s.logger.Error("GetOriginalDST failed", fields)
|
||||
return
|
||||
}
|
||||
addr = origAddr.String()
|
||||
default:
|
||||
addr = tc.LocalAddr().String()
|
||||
}
|
||||
fields["dest_addr"] = addr
|
||||
|
||||
if log.Enabled(log.LvDebug) {
|
||||
log.Debug("closing proxy connection", map[string]interface{}{
|
||||
"_dst": addr,
|
||||
})
|
||||
destConn, err := s.dialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
fields[log.FnError] = err.Error()
|
||||
s.logger.Error("failed to connect to proxy server", fields)
|
||||
return
|
||||
}
|
||||
}
|
||||
defer destConn.Close()
|
||||
|
||||
func copyData(dst io.Writer, src io.Reader, ch chan<- error) {
|
||||
_, err := io.Copy(dst, src)
|
||||
if tdst, ok := dst.(*net.TCPConn); ok {
|
||||
tdst.CloseWrite()
|
||||
s.logger.Info("proxy starts", fields)
|
||||
|
||||
// do proxy
|
||||
st := time.Now()
|
||||
env := cmd.NewEnvironment(ctx)
|
||||
env.Go(func(ctx context.Context) error {
|
||||
buf := s.pool.Get().([]byte)
|
||||
_, err := io.CopyBuffer(destConn, tc, buf)
|
||||
s.pool.Put(buf)
|
||||
if hc, ok := destConn.(netutil.HalfCloser); ok {
|
||||
hc.CloseWrite()
|
||||
}
|
||||
tc.CloseRead()
|
||||
return err
|
||||
})
|
||||
env.Go(func(ctx context.Context) error {
|
||||
buf := s.pool.Get().([]byte)
|
||||
_, err := io.CopyBuffer(tc, destConn, buf)
|
||||
s.pool.Put(buf)
|
||||
tc.CloseWrite()
|
||||
if hc, ok := destConn.(netutil.HalfCloser); ok {
|
||||
hc.CloseRead()
|
||||
}
|
||||
return err
|
||||
})
|
||||
env.Stop()
|
||||
err = env.Wait()
|
||||
|
||||
fields = cmd.FieldsFromContext(ctx)
|
||||
fields["elapsed"] = time.Since(st).Seconds()
|
||||
if err != nil {
|
||||
fields[log.FnError] = err.Error()
|
||||
s.logger.Error("proxy ends with an error", fields)
|
||||
return
|
||||
}
|
||||
if tsrc, ok := src.(*net.TCPConn); ok {
|
||||
tsrc.CloseRead()
|
||||
}
|
||||
ch <- err
|
||||
s.logger.Info("proxy ends", fields)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user