Merge pull request #18 from Snawoot/proxy_dialer

Proxy chaining support
This commit is contained in:
Snawoot 2023-02-09 22:07:06 +02:00 committed by GitHub
commit 83d76386b5
Failed to generate hash of commit
7 changed files with 290 additions and 13 deletions

View File

@ -17,6 +17,7 @@ Dumbiest HTTP proxy ever.
* Supports client authentication with client TLS certificates
* Supports HTTP/2
* Resilient to DPI (including active probing, see `hidden_domain` option for authentication providers)
* Connecting via upstream HTTP(S)/SOCKS5 proxies (proxy chaining)
## Installation
@ -167,6 +168,7 @@ Authentication parameters are passed as URI via `-auth` parameter. Scheme of URI
```
$ ~/go/bin/dumbproxy -h
Usage of /home/user/go/bin/dumbproxy:
-auth string
auth parameters (default "none://")
-autocert
@ -199,6 +201,8 @@ $ ~/go/bin/dumbproxy -h
update given htpasswd file and add/set password for username. Username and password can be passed as positional arguments or requested interactively
-passwd-cost int
bcrypt password cost (for -passwd mode) (default 4)
-proxy value
upstream proxy URL. Can be repeated multiple times to chain proxies. Examples: socks5h://127.0.0.1:9050; https://user:password@example.com:443
-timeout duration
timeout for network operations (default 10s)
-verbosity int

4
go.mod
View File

@ -3,7 +3,7 @@ module github.com/Snawoot/dumbproxy
go 1.13
require (
github.com/tg123/go-htpasswd v1.2.0 // indirect
github.com/tg123/go-htpasswd v1.2.0
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/net v0.5.0
)

35
go.sum
View File

@ -1,29 +1,52 @@
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0=
github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -10,21 +10,29 @@ import (
"time"
)
type HandlerDialer interface {
DialContext(ctx context.Context, net, address string) (net.Conn, error)
}
type ProxyHandler struct {
timeout time.Duration
auth Auth
logger *CondLogger
dialer HandlerDialer
httptransport http.RoundTripper
outbound map[string]string
outboundMux sync.RWMutex
}
func NewProxyHandler(timeout time.Duration, auth Auth, logger *CondLogger) *ProxyHandler {
httptransport := &http.Transport{}
func NewProxyHandler(timeout time.Duration, auth Auth, dialer HandlerDialer, logger *CondLogger) *ProxyHandler {
httptransport := &http.Transport{
DialContext: dialer.DialContext,
}
return &ProxyHandler{
timeout: timeout,
auth: auth,
logger: logger,
dialer: dialer,
httptransport: httptransport,
outbound: make(map[string]string),
}
@ -32,8 +40,7 @@ func NewProxyHandler(timeout time.Duration, auth Auth, logger *CondLogger) *Prox
func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
ctx, _ := context.WithTimeout(req.Context(), s.timeout)
dialer := net.Dialer{}
conn, err := dialer.DialContext(ctx, "tcp", req.RequestURI)
conn, err := s.dialer.DialContext(ctx, "tcp", req.RequestURI)
if err != nil {
s.logger.Error("Can't satisfy CONNECT request: %v", err)
http.Error(wr, "Can't satisfy CONNECT request", http.StatusBadGateway)

20
main.go
View File

@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"path/filepath"
@ -69,6 +70,7 @@ type CLIArgs struct {
passwd string
passwdCost int
positionalArgs []string
proxy []string
}
func parse_args() CLIArgs {
@ -94,6 +96,10 @@ func parse_args() CLIArgs {
flag.StringVar(&args.passwd, "passwd", "", "update given htpasswd file and add/set password for username. "+
"Username and password can be passed as positional arguments or requested interactively")
flag.IntVar(&args.passwdCost, "passwd-cost", bcrypt.MinCost, "bcrypt password cost (for -passwd mode)")
flag.Func("proxy", "upstream proxy URL. Can be repeated multiple times to chain proxies. Examples: socks5h://127.0.0.1:9050; https://user:password@example.com:443", func(p string) error {
args.proxy = append(args.proxy, p)
return nil
})
flag.Parse()
args.positionalArgs = flag.Args()
return args
@ -139,9 +145,19 @@ func run() int {
}
defer auth.Stop()
var dialer Dialer = new(net.Dialer)
for _, proxyURL := range args.proxy {
newDialer, err := proxyDialerFromURL(proxyURL, dialer)
if err != nil {
mainLogger.Critical("Failed to create dialer for proxy %q: %v", proxyURL, err)
return 3
}
dialer = newDialer
}
server := http.Server{
Addr: args.bind_address,
Handler: NewProxyHandler(args.timeout, auth, proxyLogger),
Handler: NewProxyHandler(args.timeout, auth, maybeWrapWithContextDialer(dialer), proxyLogger),
ErrorLog: log.New(logWriter, "HTTPSRV : ", log.LstdFlags|log.Lshortfile),
ReadTimeout: 0,
ReadHeaderTimeout: 0,
@ -186,7 +202,9 @@ func run() int {
}
server.TLSConfig = cfg
err = server.ListenAndServeTLS("", "")
mainLogger.Info("Proxy server started.")
} else {
mainLogger.Info("Proxy server started.")
err = server.ListenAndServe()
}
mainLogger.Critical("Server terminated with a reason: %v", err)

166
upstream.go Normal file
View File

@ -0,0 +1,166 @@
package main
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"sync"
xproxy "golang.org/x/net/proxy"
)
type HTTPProxyDialer struct {
address string
tls bool
userinfo *url.Userinfo
next ContextDialer
}
func NewHTTPProxyDialer(address string, tls bool, userinfo *url.Userinfo, next Dialer) *HTTPProxyDialer {
return &HTTPProxyDialer{
address: address,
tls: tls,
next: maybeWrapWithContextDialer(next),
userinfo: userinfo,
}
}
func HTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) {
host := u.Hostname()
port := u.Port()
tls := false
switch strings.ToLower(u.Scheme) {
case "http":
if port == "" {
port = "80"
}
case "https":
tls = true
if port == "" {
port = "443"
}
default:
return nil, errors.New("unsupported proxy type")
}
address := net.JoinHostPort(host, port)
return NewHTTPProxyDialer(address, tls, u.User, next), nil
}
func (d *HTTPProxyDialer) Dial(network, address string) (net.Conn, error) {
return d.DialContext(context.Background(), network, address)
}
func (d *HTTPProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
switch network {
case "tcp", "tcp4", "tcp6":
default:
return nil, errors.New("only \"tcp\" network is supported")
}
conn, err := d.next.DialContext(ctx, "tcp", d.address)
if err != nil {
return nil, fmt.Errorf("proxy dialer is unable to make connection: %w", err)
}
if d.tls {
hostname, _, err := net.SplitHostPort(d.address)
if err != nil {
hostname = address
}
conn = tls.Client(conn, &tls.Config{
ServerName: hostname,
})
}
stopGuardEvent := make(chan struct{})
guardErr := make(chan error, 1)
go func() {
select {
case <-stopGuardEvent:
close(guardErr)
case <-ctx.Done():
conn.Close()
guardErr <- ctx.Err()
}
}()
var stopGuardOnce sync.Once
stopGuard := func() {
stopGuardOnce.Do(func() {
close(stopGuardEvent)
})
}
defer stopGuard()
var reqBuf bytes.Buffer
fmt.Fprintf(&reqBuf, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n", address, address)
if d.userinfo != nil {
fmt.Fprintf(&reqBuf, "Proxy-Authorization: %s\r\n", basicAuthHeader(d.userinfo))
}
fmt.Fprintf(&reqBuf, "User-Agent: dumbproxy/%s\r\n\r\n", version)
_, err = io.Copy(conn, &reqBuf)
if err != nil {
conn.Close()
return nil, fmt.Errorf("unable to write proxy request for remote connection: %w", err)
}
resp, err := readResponse(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("reading proxy response failed: %w", err)
}
if resp.StatusCode != http.StatusOK {
conn.Close()
return nil, fmt.Errorf("bad status code from proxy: %d", resp.StatusCode)
}
stopGuard()
if err := <-guardErr; err != nil {
return nil, fmt.Errorf("context error: %w", err)
}
return conn, nil
}
var (
responseTerminator = []byte("\r\n\r\n")
)
func readResponse(r io.Reader) (*http.Response, error) {
var respBuf bytes.Buffer
b := make([]byte, 1)
for !bytes.HasSuffix(respBuf.Bytes(), responseTerminator) {
n, err := r.Read(b)
if err != nil {
return nil, fmt.Errorf("unable to read HTTP response: %w", err)
}
if n == 0 {
continue
}
_, err = respBuf.Write(b)
if err != nil {
return nil, fmt.Errorf("unable to store byte into buffer: %w", err)
}
}
resp, err := http.ReadResponse(bufio.NewReader(&respBuf), nil)
if err != nil {
return nil, fmt.Errorf("unable to decode proxy response: %w", err)
}
return resp, nil
}
func basicAuthHeader(userinfo *url.Userinfo) string {
username := userinfo.Username()
password, _ := userinfo.Password()
return "Basic " + base64.StdEncoding.EncodeToString(
[]byte(username+":"+password))
}

View File

@ -12,6 +12,7 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
@ -19,6 +20,7 @@ import (
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh/terminal"
xproxy "golang.org/x/net/proxy"
)
const COPY_BUF = 128 * 1024
@ -300,3 +302,60 @@ func prompt(prompt string, secure bool) (string, error) {
}
return input, nil
}
type Dialer xproxy.Dialer
type ContextDialer xproxy.ContextDialer
var registerDialerTypesOnce sync.Once
func proxyDialerFromURL(proxyURL string, forward Dialer) (Dialer, error) {
registerDialerTypesOnce.Do(func() {
xproxy.RegisterDialerType("http", HTTPProxyDialerFromURL)
xproxy.RegisterDialerType("https", HTTPProxyDialerFromURL)
})
parsedURL, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("unable to parse proxy URL: %w", err)
}
d, err := xproxy.FromURL(parsedURL, forward)
if err != nil {
return nil, fmt.Errorf("unable to construct proxy dialer from URL %q: %w", proxyURL, err)
}
return d, nil
}
type wrappedDialer struct {
d Dialer
}
func (wd wrappedDialer) Dial(net, address string) (net.Conn, error) {
return wd.d.Dial(net, address)
}
func (wd wrappedDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
var (
conn net.Conn
done = make(chan struct{}, 1)
err error
)
go func() {
conn, err = wd.d.Dial(network, address)
close(done)
if conn != nil && ctx.Err() != nil {
conn.Close()
}
}()
select {
case <-ctx.Done():
err = ctx.Err()
case <-done:
}
return conn, err
}
func maybeWrapWithContextDialer(d Dialer) ContextDialer {
if xd, ok := d.(ContextDialer); ok {
return xd
}
return wrappedDialer{d}
}