forked from ms/transocks
Initial commit
This commit is contained in:
commit
4fa2892a52
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
8
.travis.yml
Normal file
@ -0,0 +1,8 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.6
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
115
DESIGN.md
Normal file
115
DESIGN.md
Normal 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
21
LICENSE
Normal 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
119
README.md
Normal file
@ -0,0 +1,119 @@
|
||||
[][godoc]
|
||||
[](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
79
cmd/transocks/main.go
Normal 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)
|
||||
}
|
9
cmd/transocks/sample.toml
Normal file
9
cmd/transocks/sample.toml
Normal 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
58
config.go
Normal 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
122
http_tunnel.go
Normal 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
28
http_tunnel_test.go
Normal 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
87
original_dst_linux.go
Normal 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
|
||||
}
|
31
original_dst_linux_test.go
Normal file
31
original_dst_linux_test.go
Normal 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
16
original_dst_stub.go
Normal 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
124
server.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user