2
0
mirror of https://github.com/stefan01/transocks.git synced 2025-02-22 03:30:45 +07:00

Reimplement transocks based on cybozu-go/cmd .

This commit is contained in:
ymmt2005 2016-08-31 16:56:12 +09:00
parent a654def39b
commit b44c8b4e63
8 changed files with 243 additions and 151 deletions

View File

@ -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
View 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

View File

@ -1,5 +1,8 @@
[![GoDoc](https://godoc.org/github.com/cybozu-go/transocks?status.png)][godoc]
[![Build Status](https://travis-ci.org/cybozu-go/transocks.png)](https://travis-ci.org/cybozu-go/transocks)
[![GitHub release](https://img.shields.io/github/release/cybozu-go/transocks.svg?maxAge=60)][releases]
[![GoDoc](https://godoc.org/github.com/cybozu-go/transocks?status.svg)][godoc]
[![Build Status](https://travis-ci.org/cybozu-go/transocks.svg?branch=master)](https://travis-ci.org/cybozu-go/transocks)
[![Go Report Card](https://goreportcard.com/badge/github.com/cybozu-go/transocks)](https://goreportcard.com/report/github.com/cybozu-go/transocks)
[![License](https://img.shields.io/github/license/cybozu-go/transocks.svg?maxAge=2592000)](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

View File

@ -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
Listen string `toml:"listen"`
ProxyURL string `toml:"proxy_url"`
LogLevel string `toml:"log_level"`
LogFile string `toml:"log_file"`
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 {
g := &cmd.Graceful{
Listen: func() ([]net.Listener, error) {
return transocks.Listeners(c)
},
Serve: func(lns []net.Listener) {
serve(lns, c)
},
}
g.Run()
err = cmd.Wait()
if err != nil && !cmd.IsSignaled(err) {
log.ErrorExit(err)
}
defer f.Close()
log.DefaultLogger().SetOutput(f)
}
srv, err := transocks.NewServer(c)
if err != nil {
log.ErrorExit(err)
}
log.Info("server starts", nil)
srv.Serve()
log.Info("server ends", nil)
}

View File

@ -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")
}

View File

@ -10,7 +10,10 @@ import (
)
const (
// 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
)

View File

@ -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())
}

170
server.go
View File

@ -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
cmd.Server
mode Mode
logger *log.Logger
dialer proxy.Dialer
listener net.Listener
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)
func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
tc, ok := conn.(*net.TCPConn)
if !ok {
conn.Close()
panic("not a TCPConn!")
}
go s.handleConnection(tcp_conn)
}
s.logger.Error("non-TCP connection", map[string]interface{}{
"conn": conn,
})
return
}
func (s *Server) handleConnection(c *net.TCPConn) {
defer c.Close()
fields := cmd.FieldsFromContext(ctx)
fields[log.FnType] = "access"
fields["client_addr"] = conn.RemoteAddr().String()
var addr string
switch s.config.Mode {
switch s.mode {
case ModeNAT:
orig_addr, err := GetOriginalDST(c)
origAddr, err := GetOriginalDST(tc)
if err != nil {
log.Error(err.Error(), nil)
fields[log.FnError] = err.Error()
s.logger.Error("GetOriginalDST failed", fields)
return
}
addr = orig_addr.String()
addr = origAddr.String()
default:
addr = c.LocalAddr().String()
addr = tc.LocalAddr().String()
}
fields["dest_addr"] = addr
if log.Enabled(log.LvDebug) {
log.Debug("making proxy connection", map[string]interface{}{
"_dst": addr,
})
}
pconn, err := s.dialer.Dial("tcp", addr)
destConn, err := s.dialer.Dial("tcp", addr)
if err != nil {
log.Error(err.Error(), map[string]interface{}{
"_dst": addr,
})
fields[log.FnError] = err.Error()
s.logger.Error("failed to connect to proxy server", fields)
return
}
defer pconn.Close()
defer destConn.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,
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
})
break
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()
}
}
if log.Enabled(log.LvDebug) {
log.Debug("closing proxy connection", map[string]interface{}{
"_dst": addr,
return err
})
}
}
env.Stop()
err = env.Wait()
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()
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)
}