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

Initial commit

This commit is contained in:
Yamamoto, Hirotaka 2016-03-04 10:54:59 +09:00 committed by ymmt2005
commit 4fa2892a52
14 changed files with 847 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Editors
*~
.*.swp
.#*
\#*#
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

8
.travis.yml Normal file
View File

@ -0,0 +1,8 @@
sudo: false
language: go
go:
- 1.6
- tip
script:
- go test -v ./...

115
DESIGN.md Normal file
View File

@ -0,0 +1,115 @@
Design notes
============
transocks should work as a SOCKS5 client used as a transparent proxy
agent running on every hosts in trusted (i.e. data center) networks.
Destination NAT (DNAT)
----------------------
On Linux, redirecting locally-generated packet to transocks can be done
by iptables with DNAT (or REDIRECT) target.
Since DNAT/REDIRECT modifies packet's destination address, transocks
need to recover the destination address by using `getsockopt` with
`SO_ORIGINAL_DST` for IPv4 or with `IP6T_SO_ORIGINAL_DST` for IPv6.
This is, of course, Linux-specific, and Go does not provide standard
API for them.
Policy-based routing
--------------------
Except for DNAT, some operating systems provide a way to route packets
to a specific program. In order to receive such packets, the program
need to set special options on the listening socket before `bind`.
Difficult is that Go does not allow setting socket options before `bind`.
### Linux TPROXY
Linux iptables has [TPROXY][] target that can route packets to a
specific local port. The socket option is:
* IPv4
```
setsockopt(IPPROTO_IP, IP_TRANSPARENT)
```
* IPv6
```
setsockopt(IPPROTO_IPV6, IPV6_TRANSPARENT)
```
To set this option, transocks must have `CAP_NET_ADMIN` capability.
Run transocks as root user, or grant `CAP_NET_ADMIN` for the file by:
```
sudo setcap 'cap_net_admin+ep' transocks
```
### FreeBSD, NetBSD, OpenBSD
Use [PF with divert-to][pf] to route packets to a specific local port.
The listening program needs to set a socket option before `bind`:
* FreeBSD (IPv4)
```
setsockopt(IPPROTO_IP, IP_BINDANY)
```
* FreeBSD (IPv6)
```
setsockopt(IPPROTO_IPV6, IPV6_BINDANY)
```
* NetBSD, OpenBSD
```
setsockopt(SOL_SOCKET, SO_BINDANY)
```
For this to work, transocks must run as root.
Implementation strategy
-----------------------
We use Go for its efficiency and simpleness.
For SOCKS5, [golang.org/x/net/proxy][x/net] already provides SOCKS5 client.
For Linux NAT, we need to use [golang.org/x/sys/unix][x/sys] and
[unsafe.Pointer][] to use non-standard `getsockopt` options.
To set socket options before `bind`, we need to create sockets manually
by using [golang.org/x/sys/unix] and then convert the native socket to
`*net.TCPListener` by [net.FileListener][].
CONNECT tunnel
--------------
As golang.org/x/net/proxy can add custom dialers, we can implement
a proxy using http CONNECT method for tunneling through HTTP proxies
such as [Squid][].
Note that the default Squid configuration file installed for
Ubuntu 14.04 prohibits CONNECT to ports other than 443.
```
# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports
```
Remove or comment out the line to allow CONNECT to ports other than 443.
[TPROXY]: https://www.kernel.org/doc/Documentation/networking/tproxy.txt
[pf]: http://wiki.squid-cache.org/ConfigExamples/Intercept/OpenBsdPf
[x/net]: https://godoc.org/golang.org/x/net/proxy#SOCKS5
[x/sys]: https://godoc.org/golang.org/x/sys/unix
[unsafe.Pointer]: https://golang.org/pkg/unsafe/#Pointer
[net.FileListener]: https://golang.org/pkg/net/#FileListener
[Squid]: http://www.squid-cache.org/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Cybozu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

119
README.md Normal file
View File

@ -0,0 +1,119 @@
[![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)
transocks - a transparent SOCKS5/HTTP proxy
===========================================
**transocks** is a background service to redirect TCP connections
transparently to a SOCKS5 server or a HTTP proxy server like [Squid][].
Currently, transocks supports only Linux iptables with DNAT/REDIRECT target.
Features
--------
* IPv4 and IPv6
Both IPv4 and IPv6 are supported.
Note that `nf_conntrack_ipv4` or `nf_conntrack_ipv6` kernel modules
must be loaded beforehand.
* SOCKS5 and HTTP proxy (CONNECT)
We recommend using SOCKS5 server if available.
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.
* Library and executable
transocks comes with a handy executable.
You may use the library to create your own.
Usage
-----
`transocks [-h] [-f CONFIG]`
The default configuration file path is `/usr/local/etc/transocks.toml`.
`transocks` does not have *daemon* mode. Use systemd or upstart to
run it on your background.
Install
-------
Use Go 1.5 or better.
```
go get github.com/cybozu-go/transocks
go install github.com/cybozu-go/transocks/cmd/transocks
```
Configuration file format
-------------------------
`transocks.toml` is a [TOML][] file.
`listen` and `proxy_url` are mandatory.
Other items are optional.
```
# listening address of transocks.
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"
```
Redirecting connections by iptables
-----------------------------------
Use DNAT or REDIRECT target in OUTPUT chain of the `nat` table.
Save the following example to a file, then execute:
`sudo iptables-restore < FILE`
```
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:TRANSOCKS - [0:0]
-A OUTPUT -p tcp -j TRANSOCKS
-A TRANSOCKS -d 0.0.0.0/8 -j RETURN
-A TRANSOCKS -d 10.0.0.0/8 -j RETURN
-A TRANSOCKS -d 127.0.0.0/8 -j RETURN
-A TRANSOCKS -d 169.254.0.0/16 -j RETURN
-A TRANSOCKS -d 172.16.0.0/12 -j RETURN
-A TRANSOCKS -d 192.168.0.0/16 -j RETURN
-A TRANSOCKS -d 224.0.0.0/4 -j RETURN
-A TRANSOCKS -d 240.0.0.0/4 -j RETURN
-A TRANSOCKS -p tcp -j REDIRECT --to-ports 1081
COMMIT
```
Library usage
-------------
Read [the documentation][godoc].
License
-------
[MIT](https://opensource.org/licenses/MIT)
Author
------
[@ymmt2005][]
[godoc]: https://godoc.org/github.com/cybozu-go/transocks
[Squid]: http://www.squid-cache.org/
[TOML]: https://github.com/toml-lang/toml
[@ymmt2005]: https://github.com/ymmt2005

79
cmd/transocks/main.go Normal file
View File

@ -0,0 +1,79 @@
// transocks server.
package main
import (
"flag"
"fmt"
"net/url"
"os"
"github.com/BurntSushi/toml"
"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"`
}
var (
configFile = flag.String("f", "/usr/local/etc/transocks.toml",
"TOML configuration file path")
)
func loadConfig() (*transocks.Config, string, error) {
tc := new(tomlConfig)
md, err := toml.DecodeFile(*configFile, tc)
if err != nil {
return nil, "", err
}
if len(md.Undecoded()) > 0 {
return nil, "", fmt.Errorf("undecoded key in TOML: %v", md.Undecoded())
}
c := transocks.NewConfig()
c.Listen = tc.Listen
u, err := url.Parse(tc.ProxyURL)
if err != nil {
return nil, "", err
}
c.ProxyURL = u
if err = log.DefaultLogger().SetThresholdByName(tc.LogLevel); err != nil {
return nil, "", err
}
return c, tc.LogFile, nil
}
func main() {
flag.Parse()
c, logfile, 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)
}
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

@ -0,0 +1,9 @@
# This is a sample TOML file for transocks.
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 = "debug"
log_file = "/var/log/transocks.log"

58
config.go Normal file
View File

@ -0,0 +1,58 @@
package transocks
import (
"errors"
"fmt"
"net"
"net/url"
)
const (
// NAT mode
ModeNAT = "nat"
)
// Config keeps configurations for Server.
type Config struct {
// Listen is the listening address.
// e.g. "localhost:1081"
Listen string
// ProxyURL is the URL for upstream proxy.
//
// For SOCKS5, URL looks like "socks5://USER:PASSWORD@HOST:PORT".
//
// For HTTP proxy, URL looks like "http://USER:PASSWORD@HOST:PORT".
// The HTTP proxy must support CONNECT method.
ProxyURL *url.URL
// Mode determines how clients are routed to transocks.
// Default is "nat". No other options are available at this point.
Mode string
// Dialer is the base dialer to connect to the proxy server.
// The server uses the default dialer if this is nil.
Dialer *net.Dialer
}
// NewConfig creates and initializes a new Config.
func NewConfig() *Config {
c := new(Config)
c.Mode = ModeNAT
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")
}
if c.Mode != ModeNAT {
return fmt.Errorf("Unknown mode: %s", c.Mode)
}
return nil
}

122
http_tunnel.go Normal file
View File

@ -0,0 +1,122 @@
// This file provides a dialer type of "http://" scheme for
// golang.org/x/net/proxy package.
//
// The dialer type will be automatically registered by init().
//
// The dialer requests an upstream HTTP proxy to create a TCP tunnel
// by CONNECT method.
package transocks
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"time"
"golang.org/x/net/proxy"
)
func init() {
proxy.RegisterDialerType("http", httpDialType)
}
type httpDialer struct {
addr string
header http.Header
forward proxy.Dialer
}
func httpDialType(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
authz := ""
if uu := u.User; uu != nil {
passwd, _ := uu.Password()
up := uu.Username() + ":" + passwd
authz = "Basic " + base64.StdEncoding.EncodeToString([]byte(up))
}
header := map[string][]string{
"Proxy-Authorization": []string{authz},
}
return &httpDialer{
addr: u.Host,
header: header,
forward: forward,
}, nil
}
func (d *httpDialer) Dial(network, addr string) (c net.Conn, err error) {
req := &http.Request{
Method: "CONNECT",
URL: &url.URL{Opaque: addr},
Host: addr,
Header: d.header,
}
c, err = d.forward.Dial("tcp", d.addr)
if err != nil {
return
}
req.Write(c)
// Read response until "\r\n\r\n".
// bufio cannot be used as the connected server may not be
// a HTTP(S) server.
c.SetReadDeadline(time.Now().Add(10 * time.Second))
buf := make([]byte, 0, 4096)
b := make([]byte, 1)
state := 0
for {
_, e := c.Read(b)
if e != nil {
c.Close()
return nil, errors.New("reset proxy connection")
}
buf = append(buf, b[0])
switch state {
case 0:
if b[0] == byte('\r') {
state++
}
continue
case 1:
if b[0] == byte('\n') {
state++
} else {
state = 0
}
continue
case 2:
if b[0] == byte('\r') {
state++
} else {
state = 0
}
continue
case 3:
if b[0] == byte('\n') {
goto PARSE
} else {
state = 0
}
}
}
PARSE:
var zero time.Time
c.SetReadDeadline(zero)
resp, e := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(buf)), req)
if e != nil {
c.Close()
return nil, e
}
if resp.StatusCode != 200 {
c.Close()
return nil, fmt.Errorf("proxy returns %s", resp.Status)
}
return c, nil
}

28
http_tunnel_test.go Normal file
View File

@ -0,0 +1,28 @@
package transocks
import (
"io"
"net"
"os"
"testing"
"time"
)
func TestHTTPDialer(t *testing.T) {
t.Skip()
// This test only works if Squid allowing CONNECT to port 80 is
// running on the local machine on port 3128.
d := &httpDialer{
addr: "127.0.0.1:3128",
forward: &net.Dialer{Timeout: 5 * time.Second},
}
conn, err := d.Dial("tcp", "www.yahoo.com:80")
if err != nil {
t.Fatal(err)
}
conn.Write([]byte("GET / HTTP/1.1\r\nHost: www.yahoo.com:80\r\nConnection: close\r\n\r\n"))
io.Copy(os.Stdout, conn)
}

87
original_dst_linux.go Normal file
View File

@ -0,0 +1,87 @@
// +build linux
package transocks
import (
"net"
"os"
"syscall"
"unsafe"
)
const (
SO_ORIGINAL_DST = 80
IP6T_SO_ORIGINAL_DST = 80
)
func getsockopt(s int, level int, optname int, optval unsafe.Pointer, optlen *uint32) (err error) {
_, _, e := syscall.Syscall6(
syscall.SYS_GETSOCKOPT, uintptr(s), uintptr(level), uintptr(optname),
uintptr(optval), uintptr(unsafe.Pointer(optlen)), 0)
if e != 0 {
return e
}
return
}
// GetOriginalDST retrieves the original destination address from
// NATed connection. Currently, only Linux iptables using DNAT/REDIRECT
// is supported. For other operating systems, this will just return
// conn.LocalAddr().
//
// Note that this function only works when nf_conntrack_ipv4 and/or
// nf_conntrack_ipv6 is loaded in the kernel.
func GetOriginalDST(conn *net.TCPConn) (*net.TCPAddr, error) {
f, err := conn.File()
if err != nil {
return nil, err
}
defer f.Close()
fd := int(f.Fd())
// revert to non-blocking mode.
// see http://stackoverflow.com/a/28968431/1493661
if err = syscall.SetNonblock(fd, true); err != nil {
return nil, os.NewSyscallError("setnonblock", err)
}
v6 := conn.LocalAddr().(*net.TCPAddr).IP.To4() == nil
if v6 {
var addr syscall.RawSockaddrInet6
var len uint32
len = uint32(unsafe.Sizeof(addr))
err = getsockopt(fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST,
unsafe.Pointer(&addr), &len)
if err != nil {
return nil, os.NewSyscallError("getsockopt", err)
}
ip := make([]byte, 16)
for i, b := range addr.Addr {
ip[i] = b
}
pb := *(*[2]byte)(unsafe.Pointer(&addr.Port))
return &net.TCPAddr{
IP: ip,
Port: int(pb[0])*256 + int(pb[1]),
}, nil
}
// IPv4
var addr syscall.RawSockaddrInet4
var len uint32
len = uint32(unsafe.Sizeof(addr))
err = getsockopt(fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST,
unsafe.Pointer(&addr), &len)
if err != nil {
return nil, os.NewSyscallError("getsockopt", err)
}
ip := make([]byte, 4)
for i, b := range addr.Addr {
ip[i] = b
}
pb := *(*[2]byte)(unsafe.Pointer(&addr.Port))
return &net.TCPAddr{
IP: ip,
Port: int(pb[0])*256 + int(pb[1]),
}, nil
}

View File

@ -0,0 +1,31 @@
// +build linux
package transocks
import (
"net"
"testing"
)
func TestGetOriginalDST(t *testing.T) {
t.Skip()
l, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 1081})
if err != nil {
t.Fatal(err)
}
defer l.Close()
c, err := l.Accept()
if err != nil {
t.Fatal(err)
}
defer c.Close()
orig_addr, err := GetOriginalDST(c.(*net.TCPConn))
if err != nil {
t.Fatal(err)
}
t.Log(orig_addr.String())
}

16
original_dst_stub.go Normal file
View File

@ -0,0 +1,16 @@
// +build !linux
package transocks
import "net"
// GetOriginalDST retrieves the original destination address from
// NATed connection. Currently, only Linux iptables using DNAT/REDIRECT
// is supported. For other operating systems, this will just return
// conn.LocalAddr().
//
// Note that this function only works when nf_conntrack_ipv4 and/or
// nf_conntrack_ipv6 is loaded in the kernel.
func GetOriginalDST(conn *net.TCPConn) (*net.TCPAddr, error) {
return conn.LocalAddr().(*net.TCPAddr), nil
}

124
server.go Normal file
View File

@ -0,0 +1,124 @@
package transocks
import (
"io"
"net"
"time"
"github.com/cybozu-go/log"
"golang.org/x/net/proxy"
)
var (
defaultDialer = &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 60 * time.Second,
}
)
// Server provides transparent proxy server functions.
type Server struct {
config *Config
dialer proxy.Dialer
listener net.Listener
}
// NewServer creates Server.
// If c is not valid, this returns non-nil error.
func NewServer(c *Config) (*Server, error) {
if err := c.validate(); err != nil {
return nil, err
}
dialer := defaultDialer
if c.Dialer != nil {
dialer = c.Dialer
}
proxy_dialer, 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
}
return &Server{c, proxy_dialer, l}, 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(), nil)
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(err.Error(), nil)
break
}
}
if log.Enabled(log.LvDebug) {
log.Debug("closing proxy connection", map[string]interface{}{
"_dst": addr,
})
}
}
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()
}
if tsrc, ok := src.(*net.TCPConn); ok {
tsrc.CloseRead()
}
ch <- err
}