Repository: wadahiro/go-transproxy Branch: master Commit: 2ed44dd74f87 Files: 14 Total size: 41.0 KB Directory structure: gitextract_m5cqkbr0/ ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ └── transproxy/ │ └── main.go ├── common.go ├── dns.go ├── explicit.go ├── go.mod ├── go.sum ├── http.go ├── https.go ├── iptables.go └── tcp.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ bin vendor dist .DS_store ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 Hiroyuki Wada 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. ================================================ FILE: Makefile ================================================ NAME := transproxy VERSION := v0.6.0 REVISION := $(shell git rev-parse --short HEAD) SRCS := $(shell find . -type f -name '*.go') #LDFLAGS := -ldflags="-s -w -extldflags -static" DIST_DIRS := find * -type d -exec .DEFAULT_GOAL := bin/$(NAME) bin/$(NAME): $(SRCS) go build $(LDFLAGS) -o bin/$(NAME) cmd/transproxy/main.go .PHONY: clean clean: rm -rf bin/* rm -rf dist/* rm -rf vendor/* .PHONY: cross-build cross-build: for os in linux darwin; do \ [ $$os = "windows" ] && EXT=".exe"; \ for arch in amd64; do \ GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo $(LDFLAGS) -o dist/$$os-$$arch/$(NAME)$$EXT cmd/transproxy/main.go; \ done; \ done .PHONY: deps deps: GO111MODULE=on .PHONY: dist dist: cd dist && \ $(DIST_DIRS) cp ../LICENSE {} \; && \ $(DIST_DIRS) cp ../README.md {} \; && \ $(DIST_DIRS) tar -zcf $(NAME)-$(VERSION)-{}.tar.gz {} \; && \ $(DIST_DIRS) zip -r $(NAME)-$(VERSION)-{}.zip {} \; && \ cd .. .PHONY: fast fast: go build $(LDFLAGS) -o bin/$(NAME) .PHONY: install install: go install $(LDFLAGS) .PHONY: release release: git tag $(VERSION) git push origin $(VERSION) .PHONY: test test: go test -cover -v .PHONY: it it: go test -cover -v -tags integration ================================================ FILE: README.md ================================================ # go-transproxy Transparent proxy servers for HTTP, HTTPS, DNS and TCP. This repository is heavily under development. ## Description **go-transproxy** provides transparent proxy servers for HTTP, HTTPS, DNS and TCP with single binary. Nothing needs to setup many tools. Nothing needs to configure iptables. **go-transproxy** will start multiple proxy servers for these protocols. Futheremore, it will configure iptables automatically. **go-transproxy** also provides two types of explicit proxy(not transparent proxy). One is a simple proxy delegating to upstream your proxy, another is for adding `Proxy-Authorization` header automatically. ## Requirement **go-transproxy** supports only Linux iptables. ## Install ### Binaly install Download from [Releases page](https://github.com/wadahiro/go-transproxy/releases). ### Source install Use Go 1.13 for the build. ``` make ``` ## Usage ``` Usage: transproxy [options] Options: -disable-iptables Disable automatic iptables configuration -dns-over-https-enabled Use DNS-over-HTTPS service as public DNS -dns-over-https-endpoint string DNS-over-HTTPS endpoint URL (default "https://dns.google.com/resolve") -dns-over-tcp-disabled Disable DNS-over-TCP for querying to public DNS -dns-proxy-listen [host]:port DNS Proxy listen address, as [host]:port (default ":3131") -dns-tcp DNS Listen on TCP (default true) -dns-udp DNS Listen on UDP (default true) -explicit-proxy-listen [host]:port Explicit Proxy listen address for HTTP/HTTPS, as [host]:port Note: This proxy doesn't use authentication info of the `http_proxy` and `https_proxy` environment variables (default ":3132") -explicit-proxy-only Boot Explicit Proxies only -explicit-proxy-with-auth-listen [host]:port Explicit Proxy with auth listen address for HTTP/HTTPS, as [host]:port Note: This proxy uses authentication info of the `http_proxy` and `https_proxy` environment variables (default ":3133") -http-proxy-listen [host]:port HTTP Proxy listen address, as [host]:port (default ":3129") -https-proxy-listen [host]:port HTTPS Proxy listen address, as [host]:port (default ":3130") -loglevel string Log level, one of: debug, info, warn, error, fatal, panic (default "info") -private-dns string Private DNS address for no_proxy targets (IP[:port]) -public-dns string Public DNS address (IP[:port]) Note: Your proxy needs to support CONNECT method to the Public DNS port, and the public DNS needs to support TCP -tcp-proxy-dports port1,port2,... TCP Proxy dports, as port1,port2,... (default "22") -tcp-proxy-listen [host]:port TCP Proxy listen address, as [host]:port (default ":3128") ``` Proxy configuration is used from standard environment variables, `http_proxy`, `https_proxy` and `no_proxy`. Also you can use **IP Address**, **CIDR**, **Suffix Domain Name** in `no_proxy`. ### Example ``` # Set your proxy environment export http_proxy=http://foo:bar@yourproxy.example.org:3128 # Set no_proxy if you need to access directly for internal export no_proxy=example.org,192.168.0.0/24 # Start go-transproxy with admin privileges(sudo) sudo -E transproxy -private-dns 192.168.0.100 -public-dns 8.8.8.8 ``` For testing, using docker is easy way. Now, you can access to google from docker container with no proxy configuration as follows. ``` docker run --rm -it centos curl http://www.google.com 302 Moved

302 Moved

The document has moved here. ``` If your proxy doesn't support CONNECT method to DNS port, it cannot resolve public domain name transparently. Fortunately, Google privides [DNS-over-HTTPS service](https://developers.google.com/speed/public-dns/docs/dns-over-https), so you can use this service as public DNS by adding `-dns-over-https-enabled` option instead of `-public-dns` option as below even if your proxy supports CONNECT method to 443 port only. ``` sudo -E transproxy -private-dns 192.168.0.100 -dns-over-https-enabled ``` If you can resolve all domains directly from local LAN, run command without dns related options as below. It disables DNS-Proxy. ``` sudo -E transproxy ``` If you need to use both public DNS and private DNS, and need to use public DNS directly, run command with `-dns-over-tcp-disabled` option as below. It suppresses to insert a iptables OUTPUT rule for DNS over TCP. ``` sudo -E transproxy -private-dns 192.168.0.100 -public-dns 172.16.0.1 -dns-over-tcp-disabled ``` If you want to use an application which access to internet using port 5000, run command with `-tcp-proxy-dports` option as below. ``` sudo -E transproxy -private-dns 192.168.0.100 -public-dns 8.8.8.8 -tcp-proxy-dports 22,5000 ``` ## Current Limitation * HTTP proxy: Only works with HTTP host header. * HTTPS proxy: `no_proxy` only works with IP Address and CIDR if your https client doesn't support [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication). * TCP proxy: `no_proxy` only works with IP Address and CIDR. ## Licence Licensed under the [MIT](/LICENSE) license. ## Author [Hiroyuki Wada](https://github.com/wadahiro) ================================================ FILE: cmd/transproxy/main.go ================================================ package main import ( "flag" "fmt" "log" "math/rand" "net" "os" "os/signal" "path/filepath" "strconv" "strings" "syscall" "time" "github.com/comail/colog" transproxy "github.com/wadahiro/go-transproxy" ) func orPanic(err error) { if err != nil { panic(err) } } var ( fs = flag.NewFlagSet(os.Args[0], flag.ExitOnError) loglevel = fs.String( "loglevel", "info", "Log level, one of: debug, info, warn, error, fatal, panic", ) privateDNS = fs.String("private-dns", "", "Private DNS address for no_proxy targets (IP[:port])") publicDNS = fs.String("public-dns", "", "Public DNS address (IP[:port]) Note: Your proxy needs to support CONNECT method to the Public DNS port, and the public DNS needs to support TCP") tcpProxyDestPorts = fs.String( "tcp-proxy-dports", "22", "TCP Proxy dports, as `port1,port2,...`", ) tcpProxyListenAddress = fs.String( "tcp-proxy-listen", ":3128", "TCP Proxy listen address, as `[host]:port`", ) httpProxyListenAddress = fs.String( "http-proxy-listen", ":3129", "HTTP Proxy listen address, as `[host]:port`", ) httpsProxyListenAddress = fs.String( "https-proxy-listen", ":3130", "HTTPS Proxy listen address, as `[host]:port`", ) dnsProxyListenAddress = fs.String( "dns-proxy-listen", ":3131", "DNS Proxy listen address, as `[host]:port`", ) explicitProxyListenAddress = fs.String( "explicit-proxy-listen", ":3132", "Explicit Proxy listen address for HTTP/HTTPS, as `[host]:port` Note: This proxy doesn't use authentication info of the `http_proxy` and `https_proxy` environment variables", ) explicitProxyWithAuthListenAddress = fs.String( "explicit-proxy-with-auth-listen", ":3133", "Explicit Proxy with auth listen address for HTTP/HTTPS, as `[host]:port` Note: This proxy uses authentication info of the `http_proxy` and `https_proxy` environment variables", ) explicitProxyOnly = fs.Bool( "explicit-proxy-only", false, "Boot Explicit Proxies only", ) dnsOverTCPDisabled = fs.Bool( "dns-over-tcp-disabled", false, "Disable DNS-over-TCP for querying to public DNS") dnsOverHTTPSEnabled = fs.Bool( "dns-over-https-enabled", false, "Use DNS-over-HTTPS service as public DNS") dnsOverHTTPSEndpoint = fs.String( "dns-over-https-endpoint", "https://dns.google.com/resolve", "DNS-over-HTTPS endpoint URL", ) dnsEnableTCP = fs.Bool("dns-tcp", true, "DNS Listen on TCP") dnsEnableUDP = fs.Bool("dns-udp", true, "DNS Listen on UDP") disableIPTables = fs.Bool("disable-iptables", false, "Disable automatic iptables configuration") ) func main() { fs.Usage = func() { _, exe := filepath.Split(os.Args[0]) fmt.Fprint(os.Stderr, "go-transproxy.\n\n") fmt.Fprintf(os.Stderr, "Usage:\n\n %s [options]\n\nOptions:\n\n", exe) fs.PrintDefaults() } fs.Parse(os.Args[1:]) // seed the global random number generator, used in secureoperator rand.Seed(time.Now().UTC().UnixNano()) // setup logger colog.SetDefaultLevel(colog.LDebug) colog.SetMinLevel(colog.LInfo) level, err := colog.ParseLevel(*loglevel) if err != nil { log.Fatalf("alert: Invalid log level: %s", err.Error()) } colog.SetMinLevel(level) colog.SetFormatter(&colog.StdFormatter{ Colors: true, Flag: log.Ldate | log.Ltime | log.Lmicroseconds, }) colog.ParseFields(true) colog.Register() if *explicitProxyOnly { startExplicitProxyOnly(level) } else { startAllProxy(level) } } func startExplicitProxyOnly(level colog.Level) { startExplicitProxy() // serve until exit sig := make(chan os.Signal) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig log.Printf("info: Proxy servers stopping.") log.Printf("info: go-transproxy exited.") } func startAllProxy(level colog.Level) { // handling no_proxy environment noProxy := os.Getenv("no_proxy") if noProxy == "" { noProxy = os.Getenv("NO_PROXY") } np := parseNoProxy(noProxy) // start servers tcpProxy := transproxy.NewTCPProxy( transproxy.TCPProxyConfig{ ListenAddress: *tcpProxyListenAddress, NoProxy: np, }, ) if err := tcpProxy.Start(); err != nil { log.Fatalf("alert: %s", err.Error()) } dnsProxy := transproxy.NewDNSProxy( transproxy.DNSProxyConfig{ Enabled: useDNSProxy(), ListenAddress: *dnsProxyListenAddress, EnableUDP: *dnsEnableUDP, EnableTCP: *dnsEnableTCP, Endpoint: *dnsOverHTTPSEndpoint, PublicDNS: *publicDNS, PrivateDNS: *privateDNS, DNSOverHTTPSEnabled: *dnsOverHTTPSEnabled, NoProxyDomains: np.Domains, }, ) dnsProxy.Start() httpProxy := transproxy.NewHTTPProxy( transproxy.HTTPProxyConfig{ ListenAddress: *httpProxyListenAddress, NoProxy: np, Verbose: level == colog.LDebug, }, ) if err := httpProxy.Start(); err != nil { log.Fatalf("alert: %s", err.Error()) } httpsProxy := transproxy.NewHTTPSProxy( transproxy.HTTPSProxyConfig{ ListenAddress: *httpsProxyListenAddress, NoProxy: np, }, ) if err := httpsProxy.Start(); err != nil { log.Fatalf("alert: %s", err.Error()) } startExplicitProxy() log.Printf("info: All proxy servers started.") dnsToPort := toPort(*dnsProxyListenAddress) httpToPort := toPort(*httpProxyListenAddress) httpsToPort := toPort(*httpsProxyListenAddress) tcpToPort := toPort(*tcpProxyListenAddress) tcpDPorts := toPorts(*tcpProxyDestPorts) outgoingPublicDNS := *publicDNS if *dnsOverTCPDisabled { outgoingPublicDNS = "" } var t *transproxy.IPTables var err error if !*disableIPTables { t, err = transproxy.NewIPTables(&transproxy.IPTablesConfig{ DNSToPort: dnsToPort, HTTPToPort: httpToPort, HTTPSToPort: httpsToPort, TCPToPort: tcpToPort, TCPDPorts: tcpDPorts, PublicDNS: outgoingPublicDNS, }) if err != nil { log.Printf("alert: %s", err.Error()) } t.Start() log.Printf(`info: iptables rules inserted as follows. --- %s ---`, t.Show()) } // serve until exit sig := make(chan os.Signal) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig log.Printf("info: Proxy servers stopping.") // start shutdown process if !*disableIPTables { t.Stop() log.Printf("info: iptables rules deleted.") } if dnsProxy != nil { dnsProxy.Stop() } log.Printf("info: go-transproxy exited.") } func startExplicitProxy() { explicitProxyWithAuth := transproxy.NewExplicitProxy( transproxy.ExplicitProxyConfig{ ListenAddress: *explicitProxyWithAuthListenAddress, UseProxyAuthorization: true, }, ) if err := explicitProxyWithAuth.Start(); err != nil { log.Fatalf("alert: %s", err.Error()) } explicitProxy := transproxy.NewExplicitProxy( transproxy.ExplicitProxyConfig{ ListenAddress: *explicitProxyListenAddress, UseProxyAuthorization: false, }, ) if err := explicitProxy.Start(); err != nil { log.Fatalf("alert: %s", err.Error()) } } func useDNSProxy() bool { if *privateDNS == "" && *publicDNS == "" && *dnsOverHTTPSEnabled == false { return false } return true } func toPort(addr string) int { array := strings.Split(addr, ":") if len(array) != 2 { log.Printf("alert: Invalid address, no port: %s", addr) } i, err := strconv.Atoi(array[1]) if err != nil { log.Printf("alert: Invalid address, the port isn't number: %s", addr) } if i > 65535 || i < 0 { log.Printf("alert: Invalid address, the port must be an integer value in the range 0-65535: %s", addr) } return i } func toPorts(ports string) []int { array := strings.Split(ports, ",") var p []int for _, v := range array { i, err := strconv.Atoi(v) if err != nil { log.Printf("alert: Invalid port, It's not number: %s", ports) } if i > 65535 || i < 0 { log.Printf("alert: Invalid port, It must be an integer value in the range 0-65535: %s", ports) } p = append(p, i) } return p } func parseNoProxy(noProxy string) transproxy.NoProxy { p := strings.Split(noProxy, ",") var ipArray []string var cidrArray []*net.IPNet var domainArray []string for _, v := range p { ip := net.ParseIP(v) if ip != nil { ipArray = append(ipArray, v) continue } _, ipnet, err := net.ParseCIDR(v) if err == nil { cidrArray = append(cidrArray, ipnet) continue } domainArray = append(domainArray, v) } return transproxy.NoProxy{ IPs: ipArray, CIDRs: cidrArray, Domains: domainArray, } } ================================================ FILE: common.go ================================================ package transproxy import ( "fmt" "io" "log" "net" "net/http" "net/url" "strings" "sync" "github.com/cybozu-go/netutil" "github.com/cybozu-go/transocks" "os" ) type TCPListener struct { net.Listener } type TCPConn struct { *net.TCPConn OrigAddr string // ip:port } func NewTCPListener(listenAddress string) (*TCPListener, error) { l, err := net.Listen("tcp", listenAddress) if err != nil { return nil, err } return &TCPListener{ Listener: l, }, nil } func (l *TCPListener) Accept() (net.Conn, error) { c, err := l.Listener.Accept() if err != nil { return c, err } tc, ok := c.(*net.TCPConn) if !ok { return c, fmt.Errorf("Accepted non-TCP connection - %v", c) } origAddr, err := transocks.GetOriginalDST(tc) if err != nil { return c, fmt.Errorf("GetOriginalDST failed - %s", err.Error()) } log.Printf("debug: OriginalDST: %s", origAddr) log.Printf("debug: LocalAddr: %s", tc.LocalAddr().String()) log.Printf("debug: RemoteAddr: %s", tc.RemoteAddr().String()) return &TCPConn{tc, origAddr.String()}, nil } func ListenTCP(listenAddress string, handler func(tc *TCPConn)) { l, err := NewTCPListener(listenAddress) if err != nil { log.Fatalf("alert: Error listening for tcp connections - %s", err.Error()) } for { conn, err := l.Accept() // wait here if err != nil { log.Printf("warn: Error accepting new connection - %s", err.Error()) return } log.Printf("debug: Accepted new connection") go func(conn net.Conn) { defer func() { conn.Close() }() tc, _ := conn.(*TCPConn) handler(tc) }(conn) } } var pool = sync.Pool{ New: func() interface{} { return make([]byte, 64<<10) }, } func Pipe(srcConn *TCPConn, destConn net.Conn) { defer destConn.Close() log.Printf("debug: Start proxy") wg := &sync.WaitGroup{} wg.Add(1) go func() error { defer func() { wg.Done() }() buf := pool.Get().([]byte) _, err := io.CopyBuffer(destConn, srcConn, buf) pool.Put(buf) if hc, ok := destConn.(netutil.HalfCloser); ok { hc.CloseWrite() } srcConn.CloseRead() return err }() wg.Add(1) go func() error { defer func() { wg.Done() }() buf := pool.Get().([]byte) _, err := io.CopyBuffer(srcConn, destConn, buf) pool.Put(buf) srcConn.CloseWrite() if hc, ok := destConn.(netutil.HalfCloser); ok { hc.CloseRead() } return err }() wg.Wait() log.Printf("debug: End proxy") } type NoProxy struct { IPs []string CIDRs []*net.IPNet Domains []string } func httpProxyFromRule(noProxy NoProxy) func(*http.Request) (*url.URL, error) { return func(req *http.Request) (*url.URL, error) { if useProxy(noProxy, strings.Split(req.Host, ":")[0]) { return http.ProxyFromEnvironment(req) } else { return nil, nil } } } func useProxy(noProxy NoProxy, target string) bool { // TODO resolve target domain for _, d := range noProxy.Domains { if strings.HasSuffix(target, d) { log.Printf("debug: NO_PROXY: Matched no_proxy domain. Direct for %s", target) return false } } for _, ip := range noProxy.IPs { if ip == target { log.Printf("debug: NO_PROXY: Matched no_proxy ip. Direct for %s", target) return false } } for _, cidr := range noProxy.CIDRs { targetIP := net.ParseIP(target) if cidr.Contains(targetIP) { log.Printf("debug: NO_PROXY: Matched no_proxy cidr. Direct for %s", target) return false } } log.Printf("debug: Use proxy for %s", target) return true } func GetProxyEnv(key string) string { env := os.Getenv(key) if env == "" { env = os.Getenv(strings.ToUpper(key)) } return env } ================================================ FILE: dns.go ================================================ package transproxy import ( "log" "net" "strings" "sync" "time" "github.com/Sirupsen/logrus" secop "github.com/fardog/secureoperator" "github.com/miekg/dns" ) type DNSProxy struct { DNSProxyConfig udpServer *dns.Server tcpServer *dns.Server udpClient *dns.Client // used for fowarding to internal DNS tcpClient *dns.Client // used for fowarding to internal DNS waitGroup *sync.WaitGroup } type DNSProxyConfig struct { Enabled bool ListenAddress string EnableUDP bool EnableTCP bool Endpoint string PublicDNS string PrivateDNS string DNSOverHTTPSEnabled bool NoProxyDomains []string } func NewDNSProxy(c DNSProxyConfig) *DNSProxy { // Suppress standard logger for secureoperator logrus.SetLevel(logrus.ErrorLevel) // fix dns address if c.PublicDNS != "" { _, _, err := net.SplitHostPort(c.PublicDNS) if err != nil { c.PublicDNS = net.JoinHostPort(c.PublicDNS, "53") } } if c.PrivateDNS != "" { _, _, err := net.SplitHostPort(c.PrivateDNS) if err != nil { c.PrivateDNS = net.JoinHostPort(c.PrivateDNS, "53") } } // fix domains var noProxyRoutes []string for _, s := range c.NoProxyDomains { if !strings.HasSuffix(s, ".") { s += "." } noProxyRoutes = append(noProxyRoutes, s) } c.NoProxyDomains = noProxyRoutes return &DNSProxy{ DNSProxyConfig: c, udpServer: nil, tcpServer: nil, udpClient: &dns.Client{ Net: "udp", Timeout: time.Duration(10) * time.Second, SingleInflight: true, }, tcpClient: &dns.Client{ Net: "tcp", Timeout: time.Duration(10) * time.Second, SingleInflight: true, }, waitGroup: new(sync.WaitGroup), } } func (s *DNSProxy) Start() error { if !s.Enabled { log.Printf("debug: Disabled category='DNS-Proxy'") return nil } log.Printf("info: Start listener on %s category='DNS-Proxy'", s.ListenAddress) if s.DNSOverHTTPSEnabled { log.Printf("info: Use DNS-over-HTTPS service as public DNS category='DNS-Proxy'") } if !s.DNSOverHTTPSEnabled && s.PublicDNS != "" { log.Printf("info: Use %s as public DNS category='DNS-Proxy'", s.PublicDNS) } if s.PrivateDNS != "" { log.Printf("info: Use %s as private DNS for %s domains category='DNS-Proxy'", s.PrivateDNS, s.NoProxyDomains) } // Prepare external DNS handler provider, err := secop.NewGDNSProvider(s.Endpoint, &secop.GDNSOptions{ Pad: true, }) if err != nil { log.Fatal("alert: %s category='DNS-Proxy'", err) } options := &secop.HandlerOptions{} publicOverHTTPSHandler := secop.NewHandler(provider, options) // Setup DNS Handler dnsHandle := func(w dns.ResponseWriter, req *dns.Msg) { if len(req.Question) == 0 { dns.HandleFailed(w, req) return } // access logging host, _, _ := net.SplitHostPort(w.RemoteAddr().String()) log.Printf("info: category='DNS-Proxy' remoteAddr='%s' questionName='%s' questionType='%s'", host, req.Question[0].Name, dns.TypeToString[req.Question[0].Qtype]) // Resolve by proxied private DNS for _, domain := range s.NoProxyDomains { log.Printf("debug: Checking DNS route, request: %s, no_proxy: %s", req.Question[0].Name, domain) if strings.HasSuffix(req.Question[0].Name, domain) { log.Printf("debug: Matched! Routing to private DNS, request: %s, no_proxy: %s", req.Question[0].Name, domain) s.handlePrivate(w, req) return } } // Resolve by public DNS over HTTPS over http proxy if s.DNSOverHTTPSEnabled { publicOverHTTPSHandler.Handle(w, req) return } // Resolve by specified public DNS over http proxy s.handlePublic(w, req) } dns.HandleFunc(".", dnsHandle) // Start DNS Server if s.EnableUDP { s.udpServer = &dns.Server{ Addr: s.ListenAddress, Net: "udp", TsigSecret: nil, } } if s.EnableTCP { s.tcpServer = &dns.Server{ Addr: s.ListenAddress, Net: "tcp", TsigSecret: nil, } } go func() { if s.udpServer != nil { if err := s.udpServer.ListenAndServe(); err != nil { log.Fatal("alert: %s", err.Error()) } } if s.tcpServer != nil { if err := s.tcpServer.ListenAndServe(); err != nil { log.Fatal("alert: %s", err.Error()) } } }() return nil } func (s *DNSProxy) handlePublic(w dns.ResponseWriter, req *dns.Msg) { log.Printf("debug: DNS request. %#v, %s", req, req) // Need to use TCP because of using TCP-Proxy resp, _, err := s.tcpClient.Exchange(req, s.PublicDNS) if err != nil { log.Printf("warn: DNS Client failed. %s, %#v, %s", err.Error(), req, req) dns.HandleFailed(w, req) return } w.WriteMsg(resp) } func (s *DNSProxy) handlePrivate(w dns.ResponseWriter, req *dns.Msg) { var c *dns.Client if _, ok := w.RemoteAddr().(*net.TCPAddr); ok { c = s.tcpClient } else { c = s.udpClient } log.Printf("debug: DNS request. %#v, %s", req, req) resp, _, err := c.Exchange(req, s.PrivateDNS) if err != nil { log.Printf("warn: DNS Client failed. %s, %#v, %s", err.Error(), req, req) dns.HandleFailed(w, req) return } w.WriteMsg(resp) } func (s *DNSProxy) Stop() { if !s.Enabled { return } log.Printf("info: Shutting down DNS service on interrupt\n") if s.udpServer != nil { if err := s.udpServer.Shutdown(); err != nil { log.Printf("error: %s", err.Error()) } s.udpServer = nil } if s.tcpServer != nil { if err := s.tcpServer.Shutdown(); err != nil { log.Printf("error: %s", err.Error()) } s.tcpServer = nil } } ================================================ FILE: explicit.go ================================================ package transproxy import ( "encoding/base64" "io" "log" "net" "net/http" "net/url" "strings" "time" ) type ExplicitProxy struct { ExplicitProxyConfig user string category string // For HTTP proxyTransport *http.Transport proxyAuthTransport *http.Transport // For HTTPS proxyHost string proxyAuthorization string } type ExplicitProxyConfig struct { ListenAddress string UseProxyAuthorization bool } func NewExplicitProxy(c ExplicitProxyConfig) *ExplicitProxy { return &ExplicitProxy{ ExplicitProxyConfig: c, } } func (s ExplicitProxy) Start() error { u, err := url.Parse(GetProxyEnv("http_proxy")) if err != nil { return err } if s.UseProxyAuthorization { s.category = "Explicit-Proxy(Auth)" if u.User == nil { log.Printf("info: Not Started because of no proxy user category='%s'", s.category) return nil } // For HTTPS s.proxyAuthorization = "Basic " + base64.StdEncoding.EncodeToString([]byte(u.User.String())) s.proxyHost = u.Host // For HTTP s.proxyAuthTransport = &http.Transport{Proxy: http.ProxyURL(u)} } else { s.category = "Explicit-Proxy(NoAuth)" // For HTTPS s.proxyHost = u.Host // For HTTP u, _ = url.Parse(u.String()) u.User = nil s.proxyTransport = &http.Transport{Proxy: http.ProxyURL(u)} } handler := http.HandlerFunc(s.handleRequest) log.Printf("info: Start listener on %s category='%s'", s.ListenAddress, s.category) go func() { http.ListenAndServe(s.ListenAddress, handler) }() return nil } func (s ExplicitProxy) handleRequest(w http.ResponseWriter, r *http.Request) { // access logging s.accessLog(r) if r.Method == "CONNECT" { s.handleHttps(w, r) } else { s.handleHttp(w, r) } } func (s ExplicitProxy) accessLog(r *http.Request) { host, _, _ := net.SplitHostPort(r.RemoteAddr) if s.UseProxyAuthorization { log.Printf("info: category='%s' remoteAddr='%s' method='%s' uri='%s'", s.category, host, r.Method, r.RequestURI) } else { var decodedAuth string values := r.Header["Proxy-Authorization"] if len(values) > 0 { auth := strings.Split(values[0], " ") if len(auth) > 0 { b, _ := base64.StdEncoding.DecodeString(auth[1]) decodedAuth = strings.Split(string(b[:]), ":")[0] } } log.Printf("info: category='%s' user='%s' remoteAddr='%s' method='%s' uri='%s'", s.category, decodedAuth, host, r.Method, r.RequestURI) } } func (s ExplicitProxy) handleHttps(w http.ResponseWriter, r *http.Request) { destConn, err := net.DialTimeout("tcp", s.proxyHost, 10*time.Second) if err != nil { log.Printf("error: %s category='%s'", err, s.category) http.Error(w, err.Error(), http.StatusServiceUnavailable) return } hj, ok := w.(http.Hijacker) if !ok { log.Printf("error: Hijacking not supported category='%s'", s.category) http.Error(w, "Hijacking not supported", http.StatusInternalServerError) destConn.Close() return } clientConn, _, err := hj.Hijack() if err != nil { log.Printf("error: %s category='%s'", err, s.category) http.Error(w, err.Error(), http.StatusServiceUnavailable) destConn.Close() return } if s.UseProxyAuthorization { r.Header.Set("Proxy-Authorization", s.proxyAuthorization) } r.Write(destConn) go transfer(clientConn, destConn) go transfer(destConn, clientConn) log.Printf("debug: End proxy category='%s'", s.category) } func transfer(destination io.WriteCloser, source io.ReadCloser) { defer destination.Close() defer source.Close() io.Copy(destination, source) } func (s ExplicitProxy) handleHttp(w http.ResponseWriter, r *http.Request) { hj, _ := w.(http.Hijacker) var client *http.Client if s.UseProxyAuthorization { client = &http.Client{ Transport: s.proxyAuthTransport, } } else { client = &http.Client{ Transport: s.proxyTransport, } } r.RequestURI = "" if resp, err := client.Do(r); err != nil { log.Printf("error: %s", err) } else if conn, _, err := hj.Hijack(); err != nil { log.Printf("error: %s", err) } else { defer conn.Close() resp.Write(conn) } } ================================================ FILE: go.mod ================================================ module github.com/wadahiro/go-transproxy go 1.13 require ( github.com/Sirupsen/logrus v1.0.2-0.20170713114250-a3f95b5c4235 github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c github.com/coreos/go-iptables v0.2.0 github.com/cybozu-go/cmd v1.5.0 github.com/cybozu-go/log v1.4.1 github.com/cybozu-go/netutil v1.1.0 github.com/cybozu-go/transocks v1.0.0 github.com/elazarl/goproxy v0.0.0-20170413182129-aacba83f36a5 github.com/fardog/secureoperator v2.1.0+incompatible github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b github.com/miekg/dns v0.0.0-20170812192144-0598bd43cf51 github.com/pkg/errors v0.8.0 golang.org/x/net v0.0.0-20170809000501-1c05540f6879 golang.org/x/sys v0.0.0-20170814191752-2d3e384235de ) ================================================ FILE: go.sum ================================================ github.com/Sirupsen/logrus v1.0.2-0.20170713114250-a3f95b5c4235 h1:m190Z0077f/CDftqBTbt7IlAqkmvkPEafq0byCfeisg= github.com/Sirupsen/logrus v1.0.2-0.20170713114250-a3f95b5c4235/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c h1:bzYQ6WpR+t35/y19HUkolcg7SYeWZ15IclC9Z4naGHI= github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c/go.mod h1:1WwgAwMKQLYG5I2FBhpVx94YTOAuB2W59IZ7REjSE6Y= github.com/coreos/go-iptables v0.2.0 h1:RmVRALeVCicZcF3rF05e0ooU9x9TmalN0HcT4hkhG5s= github.com/coreos/go-iptables v0.2.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/cybozu-go/cmd v1.5.0 h1:ootYtwvAikKShkE6NRwAVGID7x1BrQwVH7IDeqFe8zM= github.com/cybozu-go/cmd v1.5.0/go.mod h1:VW0nNKV31B3qvEhHZI26Rjgq5afWYm6Ulai24Qkgx5M= github.com/cybozu-go/log v1.4.1 h1:EGx01xTw+ZJEM/sJcTyHm++VFqN2WUlnOi1zMOd6ZJQ= github.com/cybozu-go/log v1.4.1/go.mod h1:qS5RWuo80yknMR3he2WCWxqfVlbOuSFMz0r1mJwS018= github.com/cybozu-go/netutil v1.1.0 h1:9o33zFrGqXpexOX3hlrN4ebYwQvThr9UP33md7mGNmc= github.com/cybozu-go/netutil v1.1.0/go.mod h1:UiOBRGyD1OiABE7Hk2bXac0mqDwuG4xMQc7T5XidXRA= github.com/cybozu-go/transocks v1.0.0 h1:IyemjpF4TA/5qJd2royU4fjWBwpJKmFpi1tvCkE39dg= github.com/cybozu-go/transocks v1.0.0/go.mod h1:CXrq7WldMyROCLqg8JLxvhxxr8b8inYS/GjJ08b9UyQ= github.com/elazarl/goproxy v0.0.0-20170413182129-aacba83f36a5 h1:6hndtckUKiV+txASiPN72sRWuHJQycm6B9BI18YYmzs= github.com/elazarl/goproxy v0.0.0-20170413182129-aacba83f36a5/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/fardog/secureoperator v2.1.0+incompatible h1:l0nfc8iv87iAa6FBgRnH9wnFkfKHMnVJhQFVhaLZfQ8= github.com/fardog/secureoperator v2.1.0+incompatible/go.mod h1:N/UFQradwwlO7ZCxDMWHPRdXW6D+nnEU6nQ8Zo0o9T4= github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE= github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= github.com/miekg/dns v0.0.0-20170812192144-0598bd43cf51 h1:hZ1ngpLE9G7RaoMM6LlixdiCedgR4GhqzOYBcarqaho= github.com/miekg/dns v0.0.0-20170812192144-0598bd43cf51/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= golang.org/x/net v0.0.0-20170809000501-1c05540f6879 h1:0rFa7EaCGdQPmZVbo9F7MNF65b8dyzS6EUnXjs9Cllk= golang.org/x/net v0.0.0-20170809000501-1c05540f6879/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20170814191752-2d3e384235de/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= ================================================ FILE: http.go ================================================ package transproxy import ( "fmt" "log" "net" "net/http" "github.com/elazarl/goproxy" ) type HTTPProxy struct { HTTPProxyConfig } type HTTPProxyConfig struct { ListenAddress string NoProxy NoProxy Verbose bool } func NewHTTPProxy(c HTTPProxyConfig) *HTTPProxy { return &HTTPProxy{ HTTPProxyConfig: c, } } func (s HTTPProxy) Start() error { l, err := NewTCPListener(s.ListenAddress) if err != nil { log.Printf("error: Failed listening for tcp connections - %s category='HTTP-Proxy'", err.Error()) } proxy := goproxy.NewProxyHttpServer() proxy.Tr.Proxy = httpProxyFromRule(s.NoProxy) proxy.Verbose = s.Verbose proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { log.Printf("debug: Accept: %s, %s", req.Host, req.URL) if req.Host == "" { // TODO use origAddr from TCPCon fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") return } // Convert to proxy request (abs URL request) for passing goproxy handler req.URL.Scheme = "http" req.URL.Host = req.Host // access logging host, _, _ := net.SplitHostPort(req.RemoteAddr) log.Printf("info: category='HTTP-Proxy' remoteAddr='%s' method='%s' url='%s'", host, req.Method, req.URL) // proxy to real target proxy.ServeHTTP(w, req) }) log.Printf("info: Start listener on %s category='HTTP-Proxy'", s.ListenAddress) go func() { http.Serve(l, proxy) }() return nil } ================================================ FILE: https.go ================================================ package transproxy import ( "log" "net" "net/url" "strings" "time" "github.com/inconshreveable/go-vhost" "golang.org/x/net/proxy" ) type HTTPSProxy struct { HTTPSProxyConfig } type HTTPSProxyConfig struct { ListenAddress string NoProxy NoProxy } func NewHTTPSProxy(c HTTPSProxyConfig) *HTTPSProxy { return &HTTPSProxy{ HTTPSProxyConfig: c, } } func (s HTTPSProxy) Start() error { dialer := &net.Dialer{ KeepAlive: 3 * time.Minute, DualStack: true, } u, err := url.Parse(GetProxyEnv("http_proxy")) if err != nil { return err } pdialer, err := proxy.FromURL(u, dialer) if err != nil { return err } npdialer := proxy.Direct log.Printf("info: Start listener on %s category='HTTPS-Proxy'", s.ListenAddress) go func() { ListenTCP(s.ListenAddress, func(tc *TCPConn) { tlsConn, err := vhost.TLS(tc) if err != nil { log.Printf("error: Failed handling TLS connection - %s", err.Error()) return } defer func() { tlsConn.Free() }() origServer := tlsConn.Host() if origServer == "" { log.Printf("warn: Cannot get SNI, so fallback using `SO_ORIGINAL_DST` or `IP6T_SO_ORIGINAL_DST`") origServer = tc.OrigAddr // IPAddress:Port // TODO getting domain from origAddr, then check whether we should use proxy or not } else { log.Printf("debug: SNI: %s", origServer) origServer = net.JoinHostPort(origServer, "443") } // access logging host, _, _ := net.SplitHostPort(tc.RemoteAddr().String()) log.Printf("info: category='HTTPS-Proxy' remoteAddr='%s' method=CONNECT host='%s'", host, origServer) var destConn net.Conn if useProxy(s.NoProxy, strings.Split(origServer, ":")[0]) { destConn, err = pdialer.Dial("tcp", origServer) } else { destConn, err = npdialer.Dial("tcp", origServer) } if err != nil { log.Printf("warn: Failed to connect to destination - %s", err.Error()) return } // First, write ClientHello to real destination because we have already read it ch := tlsConn.ClientHelloMsg.Raw chSize := len(ch) chHeader := []byte{0x16, 0x03, 0x01, byte(chSize >> 8), byte(chSize)} chRecord := append(chHeader, ch...) destConn.Write(chRecord) // Then, pipe the data Pipe(tc, destConn) }) }() return nil } ================================================ FILE: iptables.go ================================================ package transproxy import ( "fmt" "net" "strconv" "strings" "github.com/coreos/go-iptables/iptables" ) const ( NAT = "nat" PREROUTING = "PREROUTING" OUTPUT = "OUTPUT" ) type IPTables struct { iptables *iptables.IPTables dnsTCPOutRule []string dnsTCPRule []string dnsUDPRule []string httpRule []string httpsRule []string tcpRule []string err error } type IPTablesConfig struct { DNSToPort int HTTPToPort int HTTPSToPort int TCPToPort int TCPDPorts []int PublicDNS string } func NewIPTables(c *IPTablesConfig) (*IPTables, error) { t, err := iptables.New() if err != nil { return nil, err } var tcpDPorts []string for _, v := range c.TCPDPorts { tcpDPorts = append(tcpDPorts, strconv.Itoa(v)) } var dnsTCPOutRule []string if c.PublicDNS != "" { h, p, err := net.SplitHostPort(c.PublicDNS) if err != nil { c.PublicDNS = net.JoinHostPort(c.PublicDNS, "53") } h, p, _ = net.SplitHostPort(c.PublicDNS) dnsTCPOutRule = []string{NAT, OUTPUT, "-p", "tcp", "-d", h, "--dport", p, "-j", "REDIRECT", "--to-ports", strconv.Itoa(c.TCPToPort)} } dnsTCPRule := []string{NAT, PREROUTING, "-p", "tcp", "--dport", "53", "-j", "REDIRECT", "--to-ports", strconv.Itoa(c.DNSToPort)} dnsUDPRule := []string{NAT, PREROUTING, "-p", "udp", "--dport", "53", "-j", "REDIRECT", "--to-ports", strconv.Itoa(c.DNSToPort)} httpRule := []string{NAT, PREROUTING, "-p", "tcp", "--dport", "80", "-j", "REDIRECT", "--to-ports", strconv.Itoa(c.HTTPToPort)} httpsRule := []string{NAT, PREROUTING, "-p", "tcp", "--dport", "443", "-j", "REDIRECT", "--to-ports", strconv.Itoa(c.HTTPSToPort)} tcpRule := []string{NAT, PREROUTING, "-p", "tcp", "-m", "multiport", "--dport", strings.Join(tcpDPorts, ","), "-j", "REDIRECT", "--to-ports", strconv.Itoa(c.TCPToPort)} return &IPTables{ iptables: t, dnsTCPOutRule: dnsTCPOutRule, dnsTCPRule: dnsTCPRule, dnsUDPRule: dnsUDPRule, httpRule: httpRule, httpsRule: httpsRule, tcpRule: tcpRule, }, nil } func (t *IPTables) Start() error { t.Check(t.dnsTCPOutRule) t.Check(t.dnsTCPRule) t.Check(t.dnsUDPRule) t.Check(t.httpRule) t.Check(t.httpsRule) t.Check(t.tcpRule) t.insertRule(t.dnsTCPOutRule) t.insertRule(t.dnsTCPRule) t.insertRule(t.dnsUDPRule) t.insertRule(t.httpRule) t.insertRule(t.httpsRule) t.insertRule(t.tcpRule) return t.err } func (t *IPTables) Stop() error { t.deleteRule(t.dnsTCPOutRule) t.deleteRule(t.dnsTCPRule) t.deleteRule(t.dnsUDPRule) t.deleteRule(t.httpRule) t.deleteRule(t.httpsRule) t.deleteRule(t.tcpRule) return t.err } func (t *IPTables) Show() string { s := fmt.Sprintf(`iptables -t %s -I %s iptables -t %s -I %s iptables -t %s -I %s iptables -t %s -I %s iptables -t %s -I %s`, t.tcpRule[0], strings.Join(t.tcpRule[1:], " "), t.httpsRule[0], strings.Join(t.httpsRule[1:], " "), t.httpRule[0], strings.Join(t.httpRule[1:], " "), t.dnsUDPRule[0], strings.Join(t.dnsUDPRule[1:], " "), t.dnsTCPRule[0], strings.Join(t.dnsTCPRule[1:], " "), ) if len(t.dnsTCPOutRule) > 0 { s += fmt.Sprintf(` iptables -t %s -I %s`, t.dnsTCPOutRule[0], strings.Join(t.dnsTCPOutRule[1:], " "), ) } return s } func (t *IPTables) Check(rule []string) { if t.err != nil || len(rule) < 3 { return } exists, err := t.iptables.Exists(rule[0], rule[1], rule[2:]...) if exists { t.err = fmt.Errorf("Same iptables rule already exists : iptables -t %s -I %s", rule[0], strings.Join(rule[1:], " ")) } if err != nil { t.err = fmt.Errorf("Checking iptables rule failed : %s", err.Error()) } } func (t *IPTables) insertRule(rule []string) { if t.err != nil || len(rule) < 3 { return } if err := t.iptables.Insert(rule[0], rule[1], 1, rule[2:]...); err != nil { t.err = fmt.Errorf("Insert iptables rule failed : %s", err.Error()) } } func (t *IPTables) deleteRule(rule []string) { // Don't skip when it has error for deleting all rules if len(rule) < 3 { return } if err := t.iptables.Delete(rule[0], rule[1], rule[2:]...); err != nil { t.err = fmt.Errorf("Delete iptables rule failed : %s", err.Error()) } } ================================================ FILE: tcp.go ================================================ package transproxy import ( "log" "net" "net/url" "strings" "time" "golang.org/x/net/proxy" ) type TCPProxy struct { TCPProxyConfig } type TCPProxyConfig struct { ListenAddress string NoProxy NoProxy } func NewTCPProxy(c TCPProxyConfig) *TCPProxy { return &TCPProxy{ TCPProxyConfig: c, } } func (s TCPProxy) Start() error { //pdialer := proxy.FromEnvironment() dialer := &net.Dialer{ KeepAlive: 3 * time.Minute, DualStack: true, } u, err := url.Parse(GetProxyEnv("http_proxy")) if err != nil { return err } pdialer, err := proxy.FromURL(u, dialer) if err != nil { return err } npdialer := proxy.Direct log.Printf("info: Start listener on %s category='TCP-Proxy'", s.ListenAddress) go func() { ListenTCP(s.ListenAddress, func(tc *TCPConn) { // access logging host, _, _ := net.SplitHostPort(tc.RemoteAddr().String()) log.Printf("info: category='TCP-Proxy' remoteAddr='%s' method=CONNECT host='%s'", host, tc.OrigAddr) var destConn net.Conn // TODO Convert OrigAddr to domain and check useProxy with domain too? if useProxy(s.NoProxy, strings.Split(tc.OrigAddr, ":")[0]) { destConn, err = pdialer.Dial("tcp", tc.OrigAddr) } else { destConn, err = npdialer.Dial("tcp", tc.OrigAddr) } if err != nil { log.Printf("error: Failed to connect to destination - %s category='TCP-Proxy'", err.Error()) return } Pipe(tc, destConn) }) }() return nil }