├── configs ├── keytabs │ └── escobar.keytab-example └── escobar.env ├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── internal ├── version │ └── version.go ├── static │ ├── config.go │ ├── static.go │ └── handlers.go ├── configs │ ├── util_windows.go │ ├── util.go │ ├── config.go │ └── config_test.go ├── proxy │ ├── config_test.go │ ├── auth.go │ ├── auth_test.go │ ├── util.go │ ├── util_test.go │ ├── config.go │ ├── proxy_test.go │ └── proxy.go └── daemon │ └── daemon.go ├── cmd └── escobar │ └── main.go ├── .goreleaser.yml ├── go.mod ├── CHANGELOG ├── README.md ├── LICENSE └── go.sum /configs/keytabs/escobar.keytab-example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savely-krasovsky/escobar/HEAD/configs/keytabs/escobar.keytab-example -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "02:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Goland (IDEA) 2 | /.idea 3 | 4 | # Personal testing config 5 | configs/keytabs/personal.keytab 6 | configs/personal.env 7 | 8 | # Binaries 9 | /dist/ 10 | escobar 11 | escobar.exe -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package version 6 | 7 | var ( 8 | // Version contains a project version. 9 | Version = "0.0.0" 10 | // Commit contains a short commit sha. 11 | Commit = "00000000" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/static/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package static 6 | 7 | import "net" 8 | 9 | type Config struct { 10 | AddrString string `long:"addr" env:"ADDR" description:"Static server address" default:"localhost:3129" json:"addr"` 11 | Addr *net.TCPAddr `no-flag:"yes" json:"-"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/configs/util_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package configs 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/L11R/escobar/internal/proxy" 10 | ) 11 | 12 | func (c *Config) CheckCredentials() error { 13 | if c.Proxy.Mode != proxy.ManualMode { 14 | return nil 15 | } 16 | 17 | if c.Proxy.DownstreamProxyAuth.Password != "" || c.Proxy.DownstreamProxyAuth.Keytab != "" { 18 | return nil 19 | } 20 | 21 | return fmt.Errorf("you should pass path keytab-file or at least password") 22 | } 23 | -------------------------------------------------------------------------------- /cmd/escobar/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "log" 9 | 10 | "github.com/L11R/escobar/internal/daemon" 11 | "github.com/kardianos/service" 12 | ) 13 | 14 | func main() { 15 | d := daemon.New() 16 | 17 | svc, err := service.New(d, &service.Config{ 18 | Name: "escobar", 19 | DisplayName: "Escobar Proxy", 20 | Description: "Local forward proxy server that helps to remove authentication. It's a Kerberos alternative to cntlm utility.", 21 | }) 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | 26 | if err := svc.Run(); err != nil { 27 | log.Fatalln(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /configs/escobar.env: -------------------------------------------------------------------------------- 1 | # Proxy 2 | ESCOBAR_PROXY_ADDR=localhost:3128 3 | ESCOBAR_PROXY_DOWNSTREAM_PROXY_URL=http://evil.corp.proxy:9090 4 | ESCOBAR_PROXY_DOWNSTREAM_PROXY_AUTH_USER=ivanovii 5 | ESCOBAR_PROXY_DOWNSTREAM_PROXY_AUTH_PASSWORD=Qwerty123 6 | ESCOBAR_PROXY_DOWNSTREAM_PROXY_AUTH_KEYTAB=/path/to/keytab 7 | ESCOBAR_PROXY_KERBEROS_REALM=EVIL.CORP 8 | ESCOBAR_PROXY_KERBEROS_KDC_ADDR=kdc.evil.corp:88 9 | ESCOBAR_PROXY_KERBEROS_KPASSWD_SERVER_ADDR=kpasswd.evil.corp:464 10 | ESCOBAR_PROXY_MODE=sspi 11 | 12 | # Proxy timeouts 13 | ESCOBAR_PROXY_SERVER_READ_TIMEOUT=0s 14 | ESCOBAR_PROXY_SERVER_READ_HEADER_TIMEOUT=30s 15 | ESCOBAR_PROXY_SERVER_WRITE_TIMEOUT=0s 16 | ESCOBAR_PROXY_SERVER_IDLE_TIMEOUT=1m 17 | ESCOBAR_PROXY_CLIENT_READ_TIMEOUT=0s 18 | ESCOBAR_PROXY_CLIENT_WRITE_TIMEOUT=0s 19 | ESCOBAR_PROXY_CLIENT_KEEPALIVE_PERIOD=1m 20 | ESCOBAR_PROXY_DOWNSTREAM_DIAL_TIMEOUT=10s 21 | ESCOBAR_PROXY_DOWNSTREAM_READ_TIMEOUT=0s 22 | ESCOBAR_PROXY_DOWNSTREAM_WRITE_TIMEOUT=0s 23 | ESCOBAR_PROXY_DOWNSTREAM_KEEPALIVE_PERIOD=1m 24 | 25 | # Static 26 | ESCOBAR_STATIC_ADDR=localhost:3129 27 | 28 | # Overall 29 | ESCOBAR_VERBOSE=TRUE -------------------------------------------------------------------------------- /internal/proxy/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "io/ioutil" 9 | "net" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | func TestKerberos_Reader(t *testing.T) { 18 | kdc := &net.TCPAddr{ 19 | IP: net.IPv4(10, 0, 0, 1), 20 | Port: 88, 21 | Zone: "", 22 | } 23 | 24 | p := NewProxy( 25 | zap.NewNop(), 26 | &Config{ 27 | Kerberos: Kerberos{ 28 | Realm: "EVIL.CORP", 29 | KDC: kdc, 30 | }, 31 | }, 32 | nil, 33 | ) 34 | 35 | expected := `[libdefaults] 36 | default_realm = EVIL.CORP 37 | 38 | [realms] 39 | EVIL.CORP = { 40 | kdc = 10.0.0.1:88 41 | }` 42 | 43 | r, err := p.config.Kerberos.Reader() 44 | require.NoError(t, err) 45 | 46 | raw, err := ioutil.ReadAll(r) 47 | require.NoError(t, err) 48 | 49 | actual := string(raw) 50 | assert.Equal(t, expected, actual) 51 | } 52 | -------------------------------------------------------------------------------- /internal/configs/util.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package configs 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/L11R/escobar/internal/proxy" 12 | ) 13 | 14 | func (c *Config) CheckCredentials() error { 15 | // Check keytab directory and file rights, it MUST NOT be too permissive 16 | if c.Proxy.DownstreamProxyAuth.Keytab != "" { 17 | dirInfo, err := os.Stat(filepath.Dir(c.Proxy.DownstreamProxyAuth.Keytab)) 18 | if err != nil { 19 | return fmt.Errorf("cannot get dir stats: %w", err) 20 | } 21 | 22 | if m := dirInfo.Mode(); m.Perm() != os.FileMode(0700) { 23 | return fmt.Errorf("keytab directory rights are too permissive") 24 | } 25 | 26 | fileInfo, err := os.Stat(c.Proxy.DownstreamProxyAuth.Keytab) 27 | if err != nil { 28 | return fmt.Errorf("cannot get file stats: %w", err) 29 | } 30 | 31 | if m := fileInfo.Mode(); m.Perm() != os.FileMode(0600) { 32 | return fmt.Errorf("keytab file rights are too permissive") 33 | } 34 | } 35 | 36 | if c.Proxy.Mode != proxy.ManualMode { 37 | return nil 38 | } 39 | 40 | if c.Proxy.DownstreamProxyAuth.Password != "" || c.Proxy.DownstreamProxyAuth.Keytab != "" { 41 | return nil 42 | } 43 | 44 | return fmt.Errorf("you should pass path keytab-file or at least password") 45 | } 46 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project_name: escobar 3 | 4 | release: 5 | github: 6 | owner: L11R 7 | name: escobar 8 | 9 | builds: 10 | - binary: escobar 11 | goos: 12 | - darwin 13 | - windows 14 | - linux 15 | - freebsd 16 | goarch: 17 | - amd64 18 | - arm64 19 | - arm 20 | - 386 21 | - ppc64le 22 | - s390x 23 | - mips64 24 | - mips64le 25 | - riscv64 26 | goarm: 27 | - 6 28 | - 7 29 | gomips: 30 | - hardfloat 31 | env: 32 | - CGO_ENABLED=0 33 | ignore: 34 | - goos: darwin 35 | goarch: 386 36 | - goos: freebsd 37 | goarch: arm64 38 | main: ./cmd/escobar/main.go 39 | flags: 40 | - -trimpath 41 | ldflags: -s -w -X $(PROJECT)/internal/version.Version={{.Version}} -X $(PROJECT)/internal/version.Commit={{.ShortCommit}} 42 | 43 | archives: 44 | - format: tar.gz 45 | wrap_in_directory: true 46 | format_overrides: 47 | - goos: windows 48 | format: zip 49 | name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 50 | files: 51 | - LICENSE 52 | - README.md 53 | 54 | snapshot: 55 | name_template: SNAPSHOT-{{ .Commit }} 56 | 57 | checksum: 58 | name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt' -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | escobar: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Setup Golang 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.18.x 18 | 19 | - name: Setup modules cache 20 | uses: actions/cache@v2 21 | with: 22 | path: ~/go/pkg/mod 23 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 24 | restore-keys: | 25 | ${{ runner.os }}-go- 26 | 27 | - uses: actions/checkout@v2 28 | 29 | - name: Download dependencies 30 | run: go mod download 31 | 32 | - name: Lint 33 | uses: golangci/golangci-lint-action@v2 34 | with: 35 | version: v1.52.2 36 | 37 | - name: Build 38 | run: go build ./cmd/escobar 39 | 40 | - name: Test 41 | run: go test -gcflags=all=-l -v ./... 42 | 43 | - name: Test race 44 | run: go test -gcflags=all=-l -race -v ./... 45 | 46 | - name: Release 47 | if: startsWith(github.ref, 'refs/tags/v') 48 | uses: goreleaser/goreleaser-action@v2 49 | with: 50 | version: latest 51 | args: release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /internal/proxy/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "encoding/base64" 9 | "fmt" 10 | "net/http" 11 | 12 | gospnego "github.com/L11R/go-spnego" 13 | "github.com/jcmturner/gokrb5/v8/spnego" 14 | ) 15 | 16 | func (p *Proxy) setProxyAuthorizationHeader(r *http.Request) error { 17 | switch p.config.Mode { 18 | case AutoMode: 19 | provider := gospnego.New() 20 | header, err := provider.GetSPNEGOHeader(p.config.DownstreamProxyURL.Hostname()) 21 | if err != nil { 22 | return fmt.Errorf("cannot get SPNEGO header: %w", err) 23 | } 24 | 25 | r.Header.Set(HeaderProxyAuthorization, header) 26 | case ManualMode: 27 | if err := spnego.SetSPNEGOHeader(p.krb5cl, r, "HTTP/"+p.config.DownstreamProxyURL.Hostname()); err != nil { 28 | return fmt.Errorf("cannot set SPNEGO header: %w", err) 29 | } 30 | 31 | r.Header.Set(HeaderProxyAuthorization, r.Header.Get(spnego.HTTPHeaderAuthRequest)) 32 | r.Header.Del(spnego.HTTPHeaderAuthRequest) 33 | case BasicMode: 34 | r.Header.Set( 35 | HeaderProxyAuthorization, 36 | "Basic "+base64.StdEncoding.EncodeToString( 37 | []byte(p.config.DownstreamProxyAuth.User+":"+p.config.DownstreamProxyAuth.Password), 38 | ), 39 | ) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/L11R/escobar 2 | 3 | go 1.18 4 | 5 | require ( 6 | bou.ke/monkey v1.0.2 7 | github.com/L11R/go-spnego v0.0.0-20220327233043-e75f5ec4d8b1 8 | github.com/L11R/httputil v0.0.0-20220615134631-4431dfe56a3f 9 | github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 10 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 11 | github.com/go-chi/chi/v5 v5.0.14 12 | github.com/jcmturner/gokrb5/v8 v8.4.4 13 | github.com/jessevdk/go-flags v1.5.0 14 | github.com/kardianos/service v1.2.2 15 | github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 16 | github.com/stretchr/testify v1.9.0 17 | github.com/undefinedlabs/go-mpatch v1.0.7 18 | go.uber.org/zap v1.27.0 19 | ) 20 | 21 | require ( 22 | github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/hashicorp/go-uuid v1.0.3 // indirect 25 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 26 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 27 | github.com/jcmturner/gofork v1.7.6 // indirect 28 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 29 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | go.uber.org/multierr v1.10.0 // indirect 32 | golang.org/x/crypto v0.21.0 // indirect 33 | golang.org/x/net v0.23.0 // indirect 34 | golang.org/x/sys v0.18.0 // indirect 35 | golang.org/x/text v0.14.0 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /internal/static/static.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package static 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "net/http" 11 | 12 | "github.com/L11R/escobar/internal/proxy" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | type Static struct { 17 | logger *zap.Logger 18 | config *Config 19 | proxyConfig *proxy.Config 20 | server *http.Server 21 | } 22 | 23 | func NewStatic(logger *zap.Logger, config *Config, proxyConfig *proxy.Config) *Static { 24 | s := &Static{ 25 | logger: logger, 26 | config: config, 27 | proxyConfig: proxyConfig, 28 | } 29 | 30 | s.server = &http.Server{ 31 | Addr: config.Addr.String(), 32 | Handler: s.newRouter(), 33 | } 34 | 35 | return s 36 | } 37 | 38 | // ListenAndServe listens and serves HTTP requests. 39 | func (s *Static) ListenAndServe() error { 40 | s.logger.Info("Listening and serving HTTP requests", zap.String("address", s.config.Addr.String())) 41 | 42 | if err := s.server.ListenAndServe(); err != nil { 43 | if !errors.Is(err, http.ErrServerClosed) { 44 | s.logger.Error("Error listening and serving HTTP requests!", zap.Error(err)) 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // Shutdown shuts down the HTTP server. 53 | func (s *Static) Shutdown(ctx context.Context) error { 54 | if err := s.server.Shutdown(ctx); err != nil { 55 | s.logger.Error("Error shutting down HTTP server!", zap.Error(err)) 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changes with Escobar 3.0.0, 29 March 2022 2 | *) Feature: service support (Windows tested, Unix and macOS should work too) 3 | *) Feature: auto mode support for Linux (macOS not supported yet) 4 | *) Change: code simplification and dependencies update 5 | *) Bugfix: many small fixes 6 | 7 | Changes with Escobar 2.2.0, 28 January 2020 8 | *) Change: Windows now require only few arguments 9 | *) Bugfix: Initialization logic changed, start is improved 10 | *) Bugfix: Tests now should not fail sometimes 11 | *) Bugfix: ENOTCONN error is now ignored 12 | 13 | Changes with Escobar 2.1.1, 14 January 2020 14 | *) Bugfix: copier now don't lose error messages 15 | 16 | Changes with Escobar 2.1.0, 14 January 2020 17 | *) Feature: keytab-file support (see README for the more details) 18 | *) Bugfix: new default http-proxy logger, less verbose 19 | 20 | Changes with Escobar 2.0.1, 13 December 2019 21 | *) Feature: basic Makefile support 22 | *) Feature: version bypassing 23 | *) Change: error handling added in static routes 24 | *) Bugfix: error handling after hijacking fix 25 | 26 | Changes with Escobar 2.0.0, 13 December 2019 27 | *) Change: tons of internal changes 28 | *) Change: kingpin changed to go-flags as more simple solution 29 | *) Change: OpenSource preparations: project name changed, company name removed from everywhere, 30 | company-specific hard-code fully removed and now is configurable 31 | *) Change: CheckAuth() reworked and now pings real URL 32 | *) Change: Try to reconnect if downstream proxy server responding 407 and immediately drops connection 33 | *) Change: Half-close TCP support during CONNECT pumping 34 | *) Feature: Server, Client and Downstream Proxy Timeouts now are configurable 35 | *) Feature: some tests added 36 | *) Bugfix: tons of bugfixes 37 | 38 | Changes with Escobar 1.0.0, 02 December 2019 39 | *) Initial testing version -------------------------------------------------------------------------------- /internal/proxy/auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/jcmturner/gokrb5/v8/client" 13 | "github.com/jcmturner/gokrb5/v8/spnego" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/undefinedlabs/go-mpatch" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | func TestProxy_setProxyAuthorizationHeader(t *testing.T) { 20 | u, _ := url.Parse("http://proxy.evil.corp:9090") 21 | 22 | p := NewProxy( 23 | zap.NewNop(), 24 | &Config{ 25 | DownstreamProxyURL: u, 26 | DownstreamProxyAuth: DownstreamProxyAuth{ 27 | User: "test_user", 28 | Password: "test_password", 29 | }, 30 | Mode: AutoMode, 31 | }, 32 | &client.Client{}, 33 | ) 34 | 35 | req, _ := http.NewRequest("GET", "https://www.google.com/", nil) 36 | 37 | t.Run("auto", func(t *testing.T) { 38 | // could not be monkey patched to test 39 | }) 40 | 41 | p.config.Mode = ManualMode 42 | 43 | t.Run("kerberos", func(t *testing.T) { 44 | expected := "Negotiate a2VyYmVyb3NfdGVzdF90b2tlbg==" 45 | 46 | patch, err := mpatch.PatchMethod(spnego.SetSPNEGOHeader, func(krb5cl *client.Client, req *http.Request, spn string) error { 47 | req.Header.Set(spnego.HTTPHeaderAuthRequest, expected) 48 | return nil 49 | }) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | // nolint:errcheck 54 | defer patch.Unpatch() 55 | if err := p.setProxyAuthorizationHeader(req); err != nil { 56 | assert.NoError(t, err) 57 | } 58 | 59 | actual := req.Header.Get(HeaderProxyAuthorization) 60 | assert.Equal(t, expected, actual) 61 | }) 62 | 63 | p.config.Mode = BasicMode 64 | 65 | t.Run("basic mode", func(t *testing.T) { 66 | expected := "Basic dGVzdF91c2VyOnRlc3RfcGFzc3dvcmQ=" 67 | 68 | if err := p.setProxyAuthorizationHeader(req); err != nil { 69 | assert.NoError(t, err) 70 | } 71 | 72 | actual := req.Header.Get(HeaderProxyAuthorization) 73 | assert.Equal(t, expected, actual) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /internal/static/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package static 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "crypto/tls" 11 | "encoding/pem" 12 | "fmt" 13 | "net/http" 14 | "net/url" 15 | "time" 16 | 17 | "github.com/go-chi/chi/v5" 18 | ) 19 | 20 | const pacFile = `function FindProxyForURL(url, host) { 21 | if (isInNet(host, "127.0.0.0", "255.0.0.0")) return "DIRECT"; 22 | else if (isInNet(host, "10.0.0.0", "255.0.0.0")) return "DIRECT"; 23 | else if (isInNet(host, "172.16.0.0", "255.240.0.0")) return "DIRECT"; 24 | else if (isInNet(host, "192.168.0.0", "255.255.0.0")) return "DIRECT"; 25 | 26 | return "PROXY %s; DIRECT"; 27 | }` 28 | 29 | func (s *Static) newRouter() http.Handler { 30 | r := chi.NewRouter() 31 | 32 | r.Get("/proxy.pac", s.pac) 33 | r.Get("/ca.crt", s.ca) 34 | 35 | return r 36 | } 37 | 38 | func (s *Static) pac(w http.ResponseWriter, _ *http.Request) { 39 | w.WriteHeader(http.StatusOK) 40 | w.Header().Set("Content-Type", "application/x-ns-proxy-autoconfig") 41 | if _, err := w.Write( 42 | []byte( 43 | fmt.Sprintf( 44 | pacFile, 45 | s.proxyConfig.Addr.String(), 46 | ), 47 | ), 48 | ); err != nil { 49 | http.Error(w, err.Error(), http.StatusInternalServerError) 50 | return 51 | } 52 | } 53 | 54 | // ca returns actual root CA certificate by doing request through our proxy 55 | func (s *Static) ca(w http.ResponseWriter, _ *http.Request) { 56 | u, err := url.Parse("http://" + s.proxyConfig.Addr.String()) 57 | if err != nil { 58 | http.Error(w, err.Error(), http.StatusInternalServerError) 59 | return 60 | } 61 | 62 | // We need http client with custom transport 63 | httpClient := http.DefaultClient 64 | 65 | tr := http.DefaultTransport 66 | // Pass our newly deployed local proxy 67 | tr.(*http.Transport).Proxy = http.ProxyURL(u) 68 | // We check it against corporate proxy, so it usually use MITM 69 | tr.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 70 | 71 | httpClient.Transport = tr 72 | 73 | req, err := http.NewRequest("GET", "https://www.google.com", nil) 74 | if err != nil { 75 | http.Error(w, err.Error(), http.StatusServiceUnavailable) 76 | return 77 | } 78 | 79 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 80 | defer cancel() 81 | req = req.WithContext(ctx) 82 | 83 | resp, err := httpClient.Do(req) 84 | if err != nil { 85 | http.Error(w, err.Error(), http.StatusServiceUnavailable) 86 | return 87 | } 88 | resp.Body.Close() 89 | 90 | buf := bytes.NewBuffer(nil) 91 | if err := pem.Encode(buf, &pem.Block{ 92 | Type: "CERTIFICATE", 93 | Bytes: resp.TLS.PeerCertificates[len(resp.TLS.PeerCertificates)-1].Raw, 94 | }); err != nil { 95 | http.Error(w, err.Error(), http.StatusInternalServerError) 96 | return 97 | } 98 | 99 | w.WriteHeader(http.StatusOK) 100 | w.Header().Set("Content-Type", "application/x-x509-ca-cert") 101 | if _, err := w.Write(buf.Bytes()); err != nil { 102 | http.Error(w, err.Error(), http.StatusInternalServerError) 103 | return 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/proxy/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net" 13 | "net/http" 14 | "os" 15 | "syscall" 16 | 17 | "go.uber.org/zap" 18 | ) 19 | 20 | // newResponse builds new HTTP responses. 21 | // If body is nil, an empty byte.Buffer will be provided to be consistent with 22 | // the guarantees provided by http.Transport and http.Client. 23 | func newResponse(code int, body io.Reader, req *http.Request) *http.Response { 24 | if body == nil { 25 | body = &bytes.Buffer{} 26 | } 27 | 28 | rc, ok := body.(io.ReadCloser) 29 | if !ok { 30 | rc = ioutil.NopCloser(body) 31 | } 32 | 33 | res := &http.Response{ 34 | StatusCode: code, 35 | Status: fmt.Sprintf("%d %s", code, http.StatusText(code)), 36 | Proto: "HTTP/1.1", 37 | ProtoMajor: 1, 38 | ProtoMinor: 1, 39 | Header: http.Header{}, 40 | Body: rc, 41 | Request: req, 42 | } 43 | 44 | if req != nil { 45 | res.Close = req.Close 46 | res.Proto = req.Proto 47 | res.ProtoMajor = req.ProtoMajor 48 | res.ProtoMinor = req.ProtoMinor 49 | } 50 | 51 | return res 52 | } 53 | 54 | type connectCopier struct { 55 | logger *zap.Logger 56 | client, backend io.ReadWriter 57 | } 58 | 59 | func (c connectCopier) copyFromBackend(errc chan<- error) { 60 | _, err := io.Copy(c.client, c.backend) 61 | 62 | if _, ok := c.client.(*net.TCPConn); ok { 63 | if err := c.client.(*net.TCPConn).CloseWrite(); err != nil { 64 | if err, ok := err.(*net.OpError).Err.(*os.SyscallError); ok { 65 | if err.Err != syscall.ENOTCONN { 66 | c.logger.Error("cannot close write", zap.Error(err)) 67 | } else { 68 | c.logger.Debug("cannot close write", zap.Error(err)) 69 | } 70 | } 71 | } 72 | } 73 | if _, ok := c.backend.(*net.TCPConn); ok { 74 | if err := c.backend.(*net.TCPConn).CloseRead(); err != nil { 75 | if err, ok := err.(*net.OpError).Err.(*os.SyscallError); ok { 76 | if err.Err != syscall.ENOTCONN { 77 | c.logger.Error("cannot close read", zap.Error(err)) 78 | } else { 79 | c.logger.Debug("cannot close write", zap.Error(err)) 80 | } 81 | } 82 | } 83 | } 84 | 85 | errc <- err 86 | } 87 | 88 | func (c connectCopier) copyToBackend(errc chan<- error) { 89 | _, err := io.Copy(c.backend, c.client) 90 | 91 | if _, ok := c.client.(*net.TCPConn); ok { 92 | if err := c.client.(*net.TCPConn).CloseRead(); err != nil { 93 | if err, ok := err.(*net.OpError).Err.(*os.SyscallError); ok { 94 | if err.Err != syscall.ENOTCONN { 95 | c.logger.Error("cannot close read", zap.Error(err)) 96 | } else { 97 | c.logger.Debug("cannot close write", zap.Error(err)) 98 | } 99 | } 100 | } 101 | } 102 | if _, ok := c.backend.(*net.TCPConn); ok { 103 | if err := c.backend.(*net.TCPConn).CloseWrite(); err != nil { 104 | if err, ok := err.(*net.OpError).Err.(*os.SyscallError); ok { 105 | if err.Err != syscall.ENOTCONN { 106 | c.logger.Error("cannot close write", zap.Error(err)) 107 | } else { 108 | c.logger.Debug("cannot close write", zap.Error(err)) 109 | } 110 | } 111 | } 112 | } 113 | 114 | errc <- err 115 | } 116 | -------------------------------------------------------------------------------- /internal/proxy/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "bytes" 9 | "net" 10 | "net/http" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/require" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | func Test_newResponse(t *testing.T) { 19 | req, err := http.NewRequest("GET", "http://www.example.com", nil) 20 | if err != nil { 21 | t.Fatalf("http.NewRequest(): got %v, want no error", err) 22 | } 23 | req.Close = true 24 | 25 | res := newResponse(200, nil, req) 26 | if got, want := res.StatusCode, 200; got != want { 27 | t.Errorf("res.StatusCode: got %d, want %d", got, want) 28 | } 29 | if got, want := res.Status, "200 OK"; got != want { 30 | t.Errorf("res.Status: got %q, want %q", got, want) 31 | } 32 | if !res.Close { 33 | t.Error("res.Close: got false, want true") 34 | } 35 | if got, want := res.Proto, "HTTP/1.1"; got != want { 36 | t.Errorf("res.Proto: got %q, want %q", got, want) 37 | } 38 | if got, want := res.ProtoMajor, 1; got != want { 39 | t.Errorf("res.ProtoMajor: got %d, want %d", got, want) 40 | } 41 | if got, want := res.ProtoMinor, 1; got != want { 42 | t.Errorf("res.ProtoMinor: got %d, want %d", got, want) 43 | } 44 | if res.Header == nil { 45 | t.Error("res.Header: got nil, want header") 46 | } 47 | if got, want := res.Request, req; got != want { 48 | t.Errorf("res.Request: got %v, want %v", got, want) 49 | } 50 | } 51 | 52 | func Test_connectCopier(t *testing.T) { 53 | backend, err := net.Listen("tcp", "localhost:4430") 54 | require.NoError(t, err) 55 | 56 | proxy, err := net.Listen("tcp", "localhost:31280") 57 | require.NoError(t, err) 58 | 59 | handleProxy := func(clientProxyConn net.Conn) { 60 | defer clientProxyConn.Close() 61 | 62 | proxyBackendConn, err := net.DialTimeout("tcp", "localhost:4430", 10*time.Second) 63 | require.NoError(t, err) 64 | defer proxyBackendConn.Close() 65 | 66 | cc := connectCopier{ 67 | logger: zap.NewNop(), 68 | client: clientProxyConn, 69 | backend: proxyBackendConn, 70 | } 71 | 72 | errc := make(chan error, 1) 73 | go cc.copyToBackend(errc) 74 | go cc.copyFromBackend(errc) 75 | require.NoError(t, <-errc) 76 | } 77 | 78 | go func() { 79 | for { 80 | conn, err := proxy.Accept() 81 | require.NoError(t, err) 82 | 83 | go handleProxy(conn) 84 | } 85 | }() 86 | 87 | handleBackend := func(clientBackendConn net.Conn) { 88 | defer clientBackendConn.Close() 89 | 90 | b := make([]byte, 1<<10) 91 | _, err := clientBackendConn.Read(b) 92 | require.NoError(t, err) 93 | 94 | if bytes.HasPrefix(b, []byte("ping")) { 95 | _, err = clientBackendConn.Write([]byte("pong")) 96 | require.NoError(t, err) 97 | 98 | return 99 | } 100 | 101 | _, err = clientBackendConn.Write([]byte("unknown command")) 102 | require.NoError(t, err) 103 | } 104 | 105 | go func() { 106 | for { 107 | conn, err := backend.Accept() 108 | require.NoError(t, err) 109 | 110 | go handleBackend(conn) 111 | } 112 | }() 113 | 114 | proxyClientConn, err := net.DialTimeout("tcp", "localhost:31280", 10*time.Second) 115 | require.NoError(t, err) 116 | defer proxyClientConn.Close() 117 | 118 | _, err = proxyClientConn.Write([]byte("ping")) 119 | require.NoError(t, err) 120 | 121 | b := make([]byte, 1<<10) 122 | _, err = proxyClientConn.Read(b) 123 | require.NoError(t, err) 124 | 125 | require.True(t, bytes.HasPrefix(b, []byte("pong"))) 126 | } 127 | -------------------------------------------------------------------------------- /internal/configs/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package configs 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "net" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/L11R/escobar/internal/proxy" 17 | "github.com/L11R/escobar/internal/static" 18 | "github.com/L11R/escobar/internal/version" 19 | "github.com/shibukawa/configdir" 20 | 21 | "github.com/jessevdk/go-flags" 22 | ) 23 | 24 | type Config struct { 25 | Proxy *proxy.Config `group:"Proxy args" namespace:"proxy" env-namespace:"ESCOBAR_PROXY" json:"proxy"` 26 | Static *static.Config `group:"Static args" namespace:"static" env-namespace:"ESCOBAR_STATIC" json:"static"` 27 | 28 | UseSystemLogger bool `short:"l" long:"syslog" description:"Enable system logger (syslog or Windows Event Log)" json:"useSystemLogger"` 29 | Install bool `long:"install" description:"Install service" json:"-"` 30 | Uninstall bool `long:"uninstall" description:"Uninstall service" json:"-"` 31 | 32 | Verbose []bool `short:"v" long:"verbose" env:"ESCOBAR_VERBOSE" description:"Verbose logs" json:"verbose"` 33 | Version func() `short:"V" long:"version" description:"Escobar version" json:"-"` 34 | } 35 | 36 | // Parse returns *Config parsed from command line arguments. 37 | func Parse() (*Config, error) { 38 | var ( 39 | config Config 40 | err error 41 | ) 42 | 43 | // Print Escobar version if -V is passed 44 | config.Version = func() { 45 | fmt.Printf("escobar version: %s-%s\n", version.Version, version.Commit) 46 | os.Exit(0) 47 | } 48 | 49 | p := flags.NewParser(&config, flags.HelpFlag|flags.PassDoubleDash) 50 | if _, err = p.Parse(); err != nil { 51 | err, ok := err.(*flags.Error) 52 | if !ok { 53 | return nil, err 54 | } 55 | 56 | if !errors.Is(err.Type, flags.ErrRequired) { 57 | return nil, err 58 | } 59 | } 60 | 61 | // Various modes require various options 62 | user := p.FindOptionByLongName("proxy.downstream-proxy-auth.user") 63 | password := p.FindOptionByLongName("proxy.downstream-proxy-auth.password") 64 | kdc := p.FindOptionByLongName("proxy.kerberos.kdc") 65 | realm := p.FindOptionByLongName("proxy.kerberos.realm") 66 | 67 | switch config.Proxy.Mode { 68 | case proxy.AutoMode: 69 | // does not require anything 70 | case proxy.ManualMode: 71 | user.Required = true 72 | kdc.Required = true 73 | realm.Required = true 74 | case proxy.BasicMode: 75 | user.Required = true 76 | password.Required = true 77 | } 78 | 79 | // Try to read config, otherwise try to parse again 80 | configDirs := configdir.New("Escobar", "Escobar") 81 | configDirs.LocalPath, _ = filepath.Abs(".") 82 | folder := configDirs.QueryFolderContainsFile("settings.json") 83 | if folder != nil && len(os.Args) == 1 { 84 | data, _ := folder.ReadFile("settings.json") 85 | if err := json.Unmarshal(data, &config); err != nil { 86 | fmt.Printf("Invalid config file: %v\n", err) 87 | os.Exit(1) 88 | } 89 | } else if _, err := p.Parse(); err != nil { 90 | return nil, err 91 | } 92 | 93 | // Parse address as *net.TCPAddr 94 | config.Proxy.Addr, err = net.ResolveTCPAddr("tcp", config.Proxy.AddrString) 95 | if err != nil { 96 | return nil, fmt.Errorf("cannot resolve proxy address: %w", err) 97 | } 98 | 99 | // Parse Downstream Proxy URL as *url.URL 100 | config.Proxy.DownstreamProxyURL, err = url.Parse(config.Proxy.DownstreamProxyURLString) 101 | if err != nil { 102 | return nil, fmt.Errorf("cannot parse downstream proxy URL: %w", err) 103 | } 104 | // Otherwise user could provide proxy.server.local:3128, it will be parsed incorrectly by url package 105 | if config.Proxy.DownstreamProxyURL.Hostname() == "" { 106 | return nil, fmt.Errorf("incorrect URL format, you are probably passing it without http://") 107 | } 108 | 109 | // Windows has different right management model 110 | if err := config.CheckCredentials(); err != nil { 111 | return nil, err 112 | } 113 | 114 | if config.Proxy.Mode == proxy.ManualMode { 115 | config.Proxy.Kerberos.KDC, err = net.ResolveTCPAddr("tcp", config.Proxy.Kerberos.KDCString) 116 | if err != nil { 117 | return nil, fmt.Errorf("cannot resolve KDC address: %w", err) 118 | } 119 | } 120 | 121 | // Parse Ping URL as *url.URL 122 | config.Proxy.PingURL, err = url.Parse(config.Proxy.PingURLString) 123 | if err != nil { 124 | return nil, fmt.Errorf("cannot parse ping URL: %w", err) 125 | } 126 | 127 | config.Static.Addr, err = net.ResolveTCPAddr("tcp", config.Static.AddrString) 128 | if err != nil { 129 | return nil, fmt.Errorf("cannot resolve static server address: %w", err) 130 | } 131 | 132 | return &config, nil 133 | } 134 | -------------------------------------------------------------------------------- /internal/configs/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package configs 6 | 7 | import ( 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/L11R/escobar/internal/proxy" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestParse(t *testing.T) { 18 | t.Run("positive", func(t *testing.T) { 19 | t.Run("short", func(t *testing.T) { 20 | os.Args = []string{ 21 | "./escobar", 22 | "-v", 23 | "-a", "localhost:31280", 24 | "-d", "http://10.0.0.1:9090", 25 | "-u", "ivanovii", 26 | "-p", "Qwerty123", 27 | "--proxy.kerberos.realm", "EVIL.CORP", 28 | "--proxy.ping-url", "https://www.google.com/", 29 | "--proxy.timeouts.server.read", "10s", 30 | "--proxy.timeouts.server.read-header", "130s", 31 | "--proxy.timeouts.server.write", "10s", 32 | "--proxy.timeouts.server.idle", "11m", 33 | "--proxy.timeouts.client.read", "10s", 34 | "--proxy.timeouts.client.write", "10s", 35 | "--proxy.timeouts.client.keepalive-period", "11m", 36 | "--proxy.timeouts.downstream.dial", "110s", 37 | "--proxy.timeouts.downstream.read", "10s", 38 | "--proxy.timeouts.downstream.write", "10s", 39 | "--proxy.timeouts.downstream.keepalive-period", "11m", 40 | "--static.addr", "localhost:31290", 41 | } 42 | 43 | config, err := Parse() 44 | require.NoError(t, err) 45 | 46 | assert.Equal(t, []bool{true}, config.Verbose) 47 | assert.Equal(t, "127.0.0.1:31280", config.Proxy.Addr.String()) 48 | assert.Equal(t, "http://10.0.0.1:9090", config.Proxy.DownstreamProxyURL.String()) 49 | assert.Equal(t, "ivanovii", config.Proxy.DownstreamProxyAuth.User) 50 | assert.Equal(t, "Qwerty123", config.Proxy.DownstreamProxyAuth.Password) 51 | assert.Equal(t, "EVIL.CORP", config.Proxy.Kerberos.Realm) 52 | assert.Equal(t, "https://www.google.com/", config.Proxy.PingURL.String()) 53 | assert.Equal(t, 10*time.Second, config.Proxy.Timeouts.Server.ReadTimeout) 54 | assert.Equal(t, 130*time.Second, config.Proxy.Timeouts.Server.ReadHeaderTimeout) 55 | assert.Equal(t, 10*time.Second, config.Proxy.Timeouts.Server.WriteTimeout) 56 | assert.Equal(t, 11*time.Minute, config.Proxy.Timeouts.Server.IdleTimeout) 57 | assert.Equal(t, 10*time.Second, config.Proxy.Timeouts.Client.ReadTimeout) 58 | assert.Equal(t, 10*time.Second, config.Proxy.Timeouts.Client.WriteTimeout) 59 | assert.Equal(t, 11*time.Minute, config.Proxy.Timeouts.Client.KeepAlivePeriod) 60 | assert.Equal(t, 110*time.Second, config.Proxy.Timeouts.DownstreamProxy.DialTimeout) 61 | assert.Equal(t, 10*time.Second, config.Proxy.Timeouts.DownstreamProxy.ReadTimeout) 62 | assert.Equal(t, 10*time.Second, config.Proxy.Timeouts.DownstreamProxy.WriteTimeout) 63 | assert.Equal(t, 11*time.Minute, config.Proxy.Timeouts.DownstreamProxy.KeepAlivePeriod) 64 | assert.Equal(t, "127.0.0.1:31290", config.Static.Addr.String()) 65 | }) 66 | 67 | t.Run("long", func(t *testing.T) { 68 | os.Args = []string{ 69 | "./escobar", 70 | "--verbose", 71 | "--proxy.addr", "localhost:31280", 72 | "--proxy.downstream-proxy-url", "http://10.0.0.1:9090", 73 | "--proxy.downstream-proxy-auth.user", "ivanovii", 74 | "--proxy.downstream-proxy-auth.password", "Qwerty123", 75 | "--proxy.kerberos.realm", "EVIL.CORP", 76 | } 77 | 78 | config, err := Parse() 79 | require.NoError(t, err) 80 | 81 | assert.Equal(t, []bool{true}, config.Verbose) 82 | assert.Equal(t, "127.0.0.1:31280", config.Proxy.Addr.String()) 83 | assert.Equal(t, "http://10.0.0.1:9090", config.Proxy.DownstreamProxyURL.String()) 84 | assert.Equal(t, "ivanovii", config.Proxy.DownstreamProxyAuth.User) 85 | assert.Equal(t, "Qwerty123", config.Proxy.DownstreamProxyAuth.Password) 86 | assert.Equal(t, "EVIL.CORP", config.Proxy.Kerberos.Realm) 87 | assert.Equal(t, "https://www.google.com/", config.Proxy.PingURL.String()) 88 | }) 89 | 90 | t.Run("manual mode is on", func(t *testing.T) { 91 | os.Args = []string{ 92 | "./escobar", 93 | "--verbose", 94 | "--proxy.addr", "localhost:31280", 95 | "--proxy.downstream-proxy-url", "http://10.0.0.1:9090", 96 | "--proxy.downstream-proxy-auth.user", "ivanovii", 97 | "--proxy.downstream-proxy-auth.password", "Qwerty123", 98 | "--proxy.kerberos.realm", "EVIL.CORP", 99 | "--proxy.kerberos.kdc", "10.0.0.1:88", 100 | "--proxy.mode", "manual", 101 | } 102 | 103 | config, err := Parse() 104 | require.NoError(t, err) 105 | 106 | assert.Equal(t, []bool{true}, config.Verbose) 107 | assert.Equal(t, "127.0.0.1:31280", config.Proxy.Addr.String()) 108 | assert.Equal(t, "http://10.0.0.1:9090", config.Proxy.DownstreamProxyURL.String()) 109 | assert.Equal(t, "ivanovii", config.Proxy.DownstreamProxyAuth.User) 110 | assert.Equal(t, "Qwerty123", config.Proxy.DownstreamProxyAuth.Password) 111 | assert.Equal(t, "EVIL.CORP", config.Proxy.Kerberos.Realm) 112 | assert.Equal(t, "10.0.0.1:88", config.Proxy.Kerberos.KDC.String()) 113 | assert.Equal(t, "https://www.google.com/", config.Proxy.PingURL.String()) 114 | assert.Equal(t, proxy.ManualMode, config.Proxy.Mode) 115 | }) 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /internal/proxy/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "net" 12 | "net/url" 13 | "text/template" 14 | "time" 15 | ) 16 | 17 | const krb5conf = `[libdefaults] 18 | default_realm = {{.Realm}} 19 | 20 | [realms] 21 | {{.Realm}} = { 22 | kdc = {{.KDC.String}} 23 | }` 24 | 25 | type Mode string 26 | 27 | const ( 28 | AutoMode Mode = "auto" 29 | ManualMode Mode = "manual" 30 | BasicMode Mode = "basic" 31 | ) 32 | 33 | type Config struct { 34 | AddrString string `short:"a" long:"addr" env:"ADDR" description:"Proxy address" default:"localhost:3128" json:"addr"` 35 | Addr *net.TCPAddr `no-flag:"yes" json:"-"` 36 | 37 | DownstreamProxyURLString string `short:"d" long:"downstream-proxy-url" env:"DOWNSTREAM_PROXY_URL" description:"Downstream proxy URL" value-name:"http://proxy.evil.corp:9090" required:"yes" json:"downstreamProxyURL"` 38 | DownstreamProxyURL *url.URL `no-flag:"yes" json:"-"` 39 | DownstreamProxyDialRetries int `short:"r" long:"downstream-proxy-dial-retries" env:"DOWNSTREAM_PROXY_DIAL_RETRIES" description:"Downstream proxy dial retries" value-name:"0" required:"no" default:"0" json:"downstreamProxyDialRetries"` 40 | 41 | DownstreamProxyAuth DownstreamProxyAuth `group:"Downstream Proxy authentication" namespace:"downstream-proxy-auth" env-namespace:"DOWNSTREAM_PROXY_AUTH" json:"downstreamProxyAuth"` 42 | 43 | Kerberos Kerberos `group:"Kerberos options" namespace:"kerberos" env-namespace:"KERBEROS" json:"kerberos"` 44 | Timeouts Timeouts `group:"Timeouts" namespace:"timeouts" env-namespace:"TIMEOUTS" json:"timeouts"` 45 | 46 | PingURLString string `long:"ping-url" env:"PING_URL" description:"URL to ping anc check credentials validity" default:"https://www.google.com/" json:"pingURL"` 47 | PingURL *url.URL `no-flag:"yes" json:"-"` 48 | 49 | Mode Mode `short:"m" long:"mode" env:"MODE" description:"Escobar mode" default:"auto" json:"mode"` 50 | } 51 | 52 | type DownstreamProxyAuth struct { 53 | User string `short:"u" long:"user" env:"USER" description:"Downstream Proxy user" json:"user"` 54 | Password string `short:"p" long:"password" env:"PASSWORD" description:"Downstream Proxy password" json:"password"` 55 | Keytab string `short:"k" long:"keytab" env:"KEYTAB" description:"Downstream Proxy path to keytab-file" json:"keytab"` 56 | } 57 | 58 | type Kerberos struct { 59 | Realm string `long:"realm" env:"REALM" description:"Kerberos realm" value-name:"EVIL.CORP" json:"realm"` 60 | 61 | KDCString string `long:"kdc" env:"KDC" description:"Key Distribution Center (KDC) address" value-name:"kdc.evil.corp:88" json:"kdc"` 62 | KDC *net.TCPAddr `no-flag:"yes" json:"-"` 63 | } 64 | 65 | func (k *Kerberos) Reader() (io.Reader, error) { 66 | tmpl, err := template.New("krb5.conf").Parse(krb5conf) 67 | if err != nil { 68 | return nil, fmt.Errorf("cannot parse template: %w", err) 69 | } 70 | 71 | buf := bytes.NewBuffer(nil) 72 | if err := tmpl.Execute(buf, k); err != nil { 73 | return nil, fmt.Errorf("cannot execute template: %w", err) 74 | } 75 | 76 | return buf, nil 77 | } 78 | 79 | type Timeouts struct { 80 | Server ServerTimeouts `group:"Server timeouts" namespace:"server" env-namespace:"SERVER" json:"server"` 81 | Client ClientTimeouts `group:"Client timeouts" namespace:"client" env-namespace:"CLIENT" json:"client"` 82 | DownstreamProxy DownstreamProxyTimeouts `group:"Downstream Proxy timeouts" namespace:"downstream" env-namespace:"DOWNSTREAM" json:"downstreamProxy"` 83 | } 84 | 85 | type ServerTimeouts struct { 86 | ReadTimeout time.Duration `long:"read" env:"READ" default:"0s" description:"HTTP server read timeout" json:"readTimeout"` 87 | ReadHeaderTimeout time.Duration `long:"read-header" env:"READ_HEADER" default:"30s" description:"HTTP server read header timeout" json:"readHeaderTimeout"` 88 | WriteTimeout time.Duration `long:"write" env:"WRITE" default:"0s" description:"HTTP server write timeout" json:"writeTimeout"` 89 | IdleTimeout time.Duration `long:"idle" env:"IDLE" default:"1m" description:"HTTP server idle timeout" json:"idleTimeout"` 90 | } 91 | 92 | type ClientTimeouts struct { 93 | ReadTimeout time.Duration `long:"read" env:"READ" default:"0s" description:"Client read timeout" json:"readTimeout"` 94 | WriteTimeout time.Duration `long:"write" env:"WRITE" default:"0s" description:"Client write timeout" json:"writeTimeout"` 95 | KeepAlivePeriod time.Duration `long:"keepalive-period" env:"KEEPALIVE_PERIOD" default:"1m" description:"Client keepalive period" json:"keepAlivePeriod"` 96 | } 97 | 98 | type DownstreamProxyTimeouts struct { 99 | DialTimeout time.Duration `long:"dial" env:"DIAL" default:"10s" description:"Downstream proxy dial timeout" json:"dialTimeout"` 100 | ReadTimeout time.Duration `long:"read" env:"READ" default:"0s" description:"Downstream proxy read timeout" json:"readTimeout"` 101 | WriteTimeout time.Duration `long:"write" env:"WRITE" default:"0s" description:"Downstream proxy write timeout" json:"writeTimeout"` 102 | KeepAlivePeriod time.Duration `long:"keepalive-period" env:"KEEPALIVE_PERIOD" default:"1m" description:"Downstream proxy keepalive period" json:"keepAlivePeriod"` 103 | } 104 | -------------------------------------------------------------------------------- /internal/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "context" 9 | "crypto/subtle" 10 | "crypto/tls" 11 | "log" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "testing" 17 | "time" 18 | 19 | "bou.ke/monkey" 20 | "github.com/L11R/httputil" 21 | "github.com/elazarl/goproxy" 22 | "github.com/elazarl/goproxy/ext/auth" 23 | "github.com/jcmturner/gokrb5/v8/client" 24 | krb5config "github.com/jcmturner/gokrb5/v8/config" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | "go.uber.org/zap" 28 | ) 29 | 30 | func TestMain(m *testing.M) { 31 | // We need corporate proxy emulation to run tests 32 | proxy := goproxy.NewProxyHttpServer() 33 | auth.ProxyBasic(proxy, "EVIL.CORP NEEDS YOUR AUTH", func(user, password string) bool { 34 | return user == "test_user" && subtle.ConstantTimeCompare([]byte(password), []byte("test_password")) == 1 35 | }) 36 | 37 | go func() { 38 | log.Fatal(http.ListenAndServe("localhost:9090", proxy)) 39 | }() 40 | 41 | exitCode := m.Run() 42 | os.Exit(exitCode) 43 | } 44 | 45 | func TestNewProxy(t *testing.T) { 46 | logger := zap.NewNop() 47 | 48 | downstreamProxyURL, _ := url.Parse("http://localhost:9090/") 49 | pingURL, _ := url.Parse("https://www.google.com/") 50 | 51 | config := &Config{ 52 | Addr: &net.TCPAddr{ 53 | IP: net.IPv4(127, 0, 0, 1), 54 | Port: 3128, 55 | }, 56 | DownstreamProxyURL: downstreamProxyURL, 57 | DownstreamProxyAuth: DownstreamProxyAuth{ 58 | User: "test_user", 59 | Password: "test_password", 60 | }, 61 | Kerberos: Kerberos{ 62 | Realm: "EVIL.CORP", 63 | KDC: &net.TCPAddr{ 64 | IP: net.IPv4(10, 0, 0, 1), 65 | Port: 88, 66 | }, 67 | }, 68 | Timeouts: Timeouts{ 69 | Server: ServerTimeouts{ 70 | ReadHeaderTimeout: 30 * time.Second, 71 | WriteTimeout: 1 * time.Minute, 72 | }, 73 | Client: ClientTimeouts{ 74 | KeepAlivePeriod: 1 * time.Minute, 75 | }, 76 | DownstreamProxy: DownstreamProxyTimeouts{ 77 | DialTimeout: 10 * time.Second, 78 | KeepAlivePeriod: 1 * time.Minute, 79 | }, 80 | }, 81 | PingURL: pingURL, 82 | } 83 | 84 | confReader, err := config.Kerberos.Reader() 85 | require.NoError(t, err) 86 | 87 | krb5conf, err := krb5config.NewFromReader(confReader) 88 | require.NoError(t, err) 89 | 90 | krb5cl := client.NewWithPassword( 91 | config.DownstreamProxyAuth.User, 92 | config.Kerberos.Realm, 93 | config.DownstreamProxyAuth.Password, 94 | krb5conf, 95 | ) 96 | 97 | // Dummy patch to avoid different pointers comparing 98 | rp := httputil.NewForwardingProxy() 99 | patch := monkey.Patch(httputil.NewForwardingProxy, func() *httputil.ReverseProxy { 100 | return rp 101 | }) 102 | // nolint:errcheck 103 | defer patch.Unpatch() 104 | 105 | expected := &Proxy{ 106 | logger: logger, 107 | config: config, 108 | krb5cl: krb5cl, 109 | httpProxy: httputil.NewForwardingProxy(), 110 | } 111 | 112 | expected.httpProxy.ErrorLog = zap.NewStdLog(logger) 113 | expected.server = &http.Server{ 114 | Addr: config.Addr.String(), 115 | Handler: expected, 116 | TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), 117 | ReadTimeout: config.Timeouts.Server.ReadTimeout, 118 | ReadHeaderTimeout: config.Timeouts.Server.ReadHeaderTimeout, 119 | WriteTimeout: config.Timeouts.Server.WriteTimeout, 120 | IdleTimeout: config.Timeouts.Server.IdleTimeout, 121 | } 122 | 123 | actual := NewProxy(logger, config, krb5cl) 124 | assert.Equal(t, expected, actual) 125 | } 126 | 127 | func TestProxy_CheckAuth(t *testing.T) { 128 | logger := zap.NewNop() 129 | 130 | downstreamProxyURL, _ := url.Parse("http://localhost:9090/") 131 | httpPingURL, _ := url.Parse("http://checkip.amazonaws.com/") 132 | httpsPingURL, _ := url.Parse("https://checkip.amazonaws.com/") 133 | 134 | config := &Config{ 135 | Addr: &net.TCPAddr{ 136 | IP: net.IPv4(127, 0, 0, 1), 137 | Port: 31280, 138 | }, 139 | DownstreamProxyURL: downstreamProxyURL, 140 | DownstreamProxyAuth: DownstreamProxyAuth{ 141 | User: "test_user", 142 | Password: "test_password", 143 | }, 144 | Timeouts: Timeouts{ 145 | Server: ServerTimeouts{ 146 | ReadHeaderTimeout: 30 * time.Second, 147 | WriteTimeout: 1 * time.Minute, 148 | }, 149 | Client: ClientTimeouts{ 150 | KeepAlivePeriod: 1 * time.Minute, 151 | }, 152 | DownstreamProxy: DownstreamProxyTimeouts{ 153 | DialTimeout: 10 * time.Second, 154 | KeepAlivePeriod: 1 * time.Minute, 155 | }, 156 | }, 157 | PingURL: httpPingURL, 158 | Mode: BasicMode, 159 | } 160 | 161 | p := NewProxy(logger, config, nil) 162 | 163 | l, err := p.Listen() 164 | require.NoError(t, err) 165 | 166 | go func() { 167 | require.NoError(t, p.Serve(l)) 168 | }() 169 | 170 | t.Run("http", func(t *testing.T) { 171 | ok, err := p.CheckAuth() 172 | require.NoError(t, err) 173 | require.True(t, ok) 174 | }) 175 | 176 | p.config.PingURL = httpsPingURL 177 | 178 | t.Run("https", func(t *testing.T) { 179 | ok, err := p.CheckAuth() 180 | require.NoError(t, err) 181 | require.True(t, ok) 182 | }) 183 | 184 | require.NoError(t, p.Shutdown(context.Background())) 185 | } 186 | -------------------------------------------------------------------------------- /internal/daemon/daemon.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/L11R/escobar/internal/configs" 13 | "github.com/L11R/escobar/internal/proxy" 14 | "github.com/L11R/escobar/internal/static" 15 | "github.com/jcmturner/gokrb5/v8/client" 16 | krb5config "github.com/jcmturner/gokrb5/v8/config" 17 | "github.com/jcmturner/gokrb5/v8/keytab" 18 | "github.com/jessevdk/go-flags" 19 | "github.com/kardianos/service" 20 | "github.com/shibukawa/configdir" 21 | "go.uber.org/zap" 22 | "go.uber.org/zap/zapcore" 23 | ) 24 | 25 | type Daemon struct { 26 | SystemLogger service.Logger 27 | 28 | logger *zap.Logger 29 | config *configs.Config 30 | 31 | proxy *proxy.Proxy 32 | static *static.Static 33 | } 34 | 35 | func New() *Daemon { 36 | return &Daemon{} 37 | } 38 | 39 | func (d *Daemon) Start(svc service.Service) error { 40 | go d.run(svc) 41 | return nil 42 | } 43 | 44 | func (d *Daemon) run(svc service.Service) { 45 | // Parse command line arguments, environment variables or from config file 46 | config, err := configs.Parse() 47 | if err != nil { 48 | if err, ok := err.(*flags.Error); ok { 49 | fmt.Println(err) 50 | os.Exit(0) 51 | } 52 | 53 | fmt.Printf("Invalid args: %v\n", err) 54 | os.Exit(1) 55 | } 56 | d.config = config 57 | 58 | // Init loggers 59 | enabler := zapcore.ErrorLevel 60 | if len(config.Verbose) != 0 && config.Verbose[0] { 61 | enabler = zapcore.DebugLevel 62 | } 63 | loggers := []zapcore.Core{ 64 | zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), enabler), 65 | } 66 | 67 | // Attach system logger if enabled 68 | syslogErrChan := make(chan error, 1) 69 | if config.UseSystemLogger { 70 | sysLogger, err := svc.SystemLogger(syslogErrChan) 71 | if err != nil { 72 | log.Fatalln(err) 73 | } 74 | d.SystemLogger = sysLogger 75 | 76 | loggers = append( 77 | loggers, 78 | zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(d), enabler), 79 | ) 80 | } else { 81 | close(syslogErrChan) 82 | } 83 | logger := zap.New(zapcore.NewTee(loggers...)) 84 | d.logger = logger 85 | go func() { 86 | for err := range syslogErrChan { 87 | logger.Error("Error from system logger received", zap.Error(err)) 88 | } 89 | }() 90 | 91 | var krb5cl *client.Client 92 | if config.Proxy.Mode == proxy.ManualMode { 93 | krb5cl, err = d.initKrb5() 94 | if err != nil { 95 | log.Fatalln(err) 96 | } 97 | } 98 | 99 | p := proxy.NewProxy(logger, config.Proxy, krb5cl) 100 | d.proxy = p 101 | s := static.NewStatic(logger, config.Static, config.Proxy) 102 | d.static = s 103 | 104 | if config.Install { 105 | b, _ := json.MarshalIndent(config, "", "\t") 106 | 107 | configDirs := configdir.New("Escobar", "Escobar") 108 | folders := configDirs.QueryFolders(configdir.System) 109 | if err := folders[0].WriteFile("settings.json", b); err != nil { 110 | logger.Error("Error while trying to save config!", zap.Error(err)) 111 | } 112 | 113 | if err := svc.Install(); err != nil { 114 | logger.Error("Error while trying to install service!", zap.Error(err)) 115 | } 116 | return 117 | } 118 | if config.Uninstall { 119 | if err := svc.Uninstall(); err != nil { 120 | logger.Error("Error while trying to uninstall service!", zap.Error(err)) 121 | } 122 | return 123 | } 124 | 125 | l, err := p.Listen() 126 | if err != nil { 127 | logger.Fatal("Cannot listen socket!", zap.Error(err)) 128 | } 129 | 130 | errChan := make(chan error, 1) 131 | 132 | go func() { 133 | errChan <- p.Serve(l) 134 | }() 135 | 136 | go func() { 137 | errChan <- s.ListenAndServe() 138 | }() 139 | 140 | go func() { 141 | // Check auth against out real server 142 | ok, err := p.CheckAuth() 143 | if err != nil { 144 | logger.Error( 145 | "Cannot check downstream proxy!", 146 | zap.String("ping_url", config.Proxy.PingURL.String()), 147 | zap.Error(err), 148 | ) 149 | 150 | errChan <- err 151 | return 152 | } 153 | 154 | if !ok { 155 | errChan <- errors.New("provided credentials for downstream proxy are invalid") 156 | } 157 | }() 158 | 159 | go func() { 160 | if err := <-errChan; err != nil { 161 | logger.Error("Error while running proxy!", zap.Error(err)) 162 | // nolint:errcheck 163 | d.Stop(nil) 164 | os.Exit(1) 165 | } 166 | }() 167 | } 168 | 169 | // Stop shutdowns proxy and static server 170 | func (d *Daemon) Stop(_ service.Service) error { 171 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 172 | defer cancel() 173 | 174 | d.logger.Info("Stopping proxy...") 175 | 176 | // Close static server first, it shouldn't have many open connections 177 | if err := d.static.Shutdown(ctx); err != nil { 178 | return fmt.Errorf("error while shutting down the static server: %w", err) 179 | } 180 | 181 | if err := d.proxy.Shutdown(ctx); err != nil { 182 | return fmt.Errorf("error while shutting down the proxy server: %w", err) 183 | } 184 | 185 | d.logger.Info("Proxy stopped") 186 | return nil 187 | } 188 | 189 | // Write implements io.Writer to create zap encoder 190 | func (d *Daemon) Write(b []byte) (int, error) { 191 | var entry struct { 192 | Level zapcore.Level `json:"level"` 193 | } 194 | if err := json.Unmarshal(b, &entry); err != nil { 195 | return 0, err 196 | } 197 | 198 | switch entry.Level { 199 | case zapcore.DebugLevel: 200 | return 0, d.SystemLogger.Info(string(b)) 201 | case zapcore.InfoLevel: 202 | return 0, d.SystemLogger.Info(string(b)) 203 | case zapcore.WarnLevel: 204 | return 0, d.SystemLogger.Warning(string(b)) 205 | default: 206 | return 0, d.SystemLogger.Error(string(b)) 207 | } 208 | } 209 | 210 | // initKrb5 creates Kerberos client with user credentials if we are using Linux, macOS or something else. 211 | func (d *Daemon) initKrb5() (*client.Client, error) { 212 | // Create Kerberos configuration for client 213 | r, err := d.config.Proxy.Kerberos.Reader() 214 | if err != nil { 215 | return nil, fmt.Errorf("cannot create Kerberos config: %w", err) 216 | } 217 | 218 | kbr5conf, err := krb5config.NewFromReader(r) 219 | if err != nil { 220 | return nil, fmt.Errorf("cannot read Kerberos config: %w", err) 221 | } 222 | 223 | if d.config.Proxy.DownstreamProxyAuth.Keytab != "" { 224 | kt, err := keytab.Load(d.config.Proxy.DownstreamProxyAuth.Keytab) 225 | if err != nil { 226 | return nil, fmt.Errorf("cannot read Keytab-file: %w", err) 227 | } 228 | 229 | return client.NewWithKeytab( 230 | d.config.Proxy.DownstreamProxyAuth.User, 231 | d.config.Proxy.Kerberos.Realm, 232 | kt, 233 | kbr5conf, 234 | client.DisablePAFXFAST(true), 235 | ), nil 236 | } 237 | 238 | return client.NewWithPassword( 239 | d.config.Proxy.DownstreamProxyAuth.User, 240 | d.config.Proxy.Kerberos.Realm, 241 | d.config.Proxy.DownstreamProxyAuth.Password, 242 | kbr5conf, 243 | client.DisablePAFXFAST(true), 244 | ), nil 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Escobar 2 | [![Go](https://github.com/L11R/escobar/actions/workflows/go.yml/badge.svg)](https://github.com/L11R/escobar/actions/workflows/go.yml) 3 | 4 | This is an alternative to `cntlm` utility, but written in Go. It's aim to be simpler and easier to customize 5 | for own needs. Mainly tested against McAfee Web Gateway. Tested in Linux, macOS and Windows, but should work on other 6 | OS like Android. 7 | 8 | Unlike `cntlm` it uses Kerberos-based authorization. It also supports Basic Authorization (as dedicated mode), 9 | this is useful while KDC is unavailable (e.g. while using VPN). 10 | 11 | As an extra feature it deploys small static server with two routes: 12 | 1. `GET /proxy.pac` — simple PAC-file (Proxy Auto-Configuration). 13 | 2. `GET /ca.crt` — always actual root certificate. Useful during first setup to retrieve Man-In-The-Middle root 14 | certificate (corporate proxy in our case) and add it as trusted. 15 | 16 | ### Testing 17 | Project uses monkey patching, so to test it you need to turn off inlining: 18 | ```bash 19 | go test -gcflags=-l ./... 20 | ``` 21 | 22 | ### As service 23 | Escobar could work as service as well. At first, you need to install it. 24 | ```bash 25 | sudo escobar -d http://proxy.evil.corp:9090/ --install 26 | ``` 27 | After installing service, it will create config file from CLI parameters: 28 | 29 | | Windows | Linux/BSD | macOS | 30 | |-----------------------------------------------|------------------------------------------------------------|--------------------------------------------------------------| 31 | | `%PROGRAMDATA%\Escobar\Escobar\settings.json` | `${XDG_CONFIG_DIRS}/etc/xdg/Escobar/Escobar/settings.json` | `/Library/Application Support/Escobar/Escobar/settings.json` | 32 | 33 | `settings.json` file example: 34 | ```json 35 | { 36 | "proxy": { 37 | "addr": "localhost:3128", 38 | "downstreamProxyURL": "http://proxy.evil.corp:9090/", 39 | "downstreamProxyDialRetries": 0, 40 | "downstreamProxyAuth": { 41 | "user": "", 42 | "password": "", 43 | "keytab": "" 44 | }, 45 | "kerberos": { 46 | "realm": "", 47 | "kdc": "" 48 | }, 49 | "timeouts": { 50 | "server": { 51 | "readTimeout": 0, 52 | "readHeaderTimeout": 30000000000, 53 | "writeTimeout": 0, 54 | "idleTimeout": 60000000000 55 | }, 56 | "client": { 57 | "readTimeout": 0, 58 | "writeTimeout": 0, 59 | "keepAlivePeriod": 60000000000 60 | }, 61 | "downstreamProxy": { 62 | "dialTimeout": 10000000000, 63 | "readTimeout": 0, 64 | "writeTimeout": 0, 65 | "keepAlivePeriod": 60000000000 66 | } 67 | }, 68 | "pingURL": "https://www.google.com/", 69 | "mode": "auto" 70 | }, 71 | "static": { 72 | "addr": "localhost:3129" 73 | }, 74 | "useSystemLogger": true, 75 | "verbose": [ 76 | true 77 | ] 78 | } 79 | ``` 80 | 81 | ### Status 82 | This app provides as is and proper work could not be guaranteed. 83 | You are free to contribute by creating issues and PR. 84 | 85 | ### Using 86 | Run `escobar --help` to get this detailed help: 87 | ``` 88 | Usage: 89 | escobar [OPTIONS] 90 | 91 | Application Options: 92 | /l, /syslog Enable system logger (syslog or Windows Event Log) 93 | /install Install service 94 | /uninstall Uninstall service 95 | /v, /verbose Verbose logs [%ESCOBAR_VERBOSE%] 96 | /V, /version Escobar version 97 | 98 | Proxy args: 99 | /a, /proxy.addr: Proxy address (default: localhost:3128) [%ESCOBAR_PROXY_ADDR%] 100 | /d, /proxy.downstream-proxy-url:http://proxy.evil.corp:9090 Downstream proxy URL [%ESCOBAR_PROXY_DOWNSTREAM_PROXY_URL%] 101 | /r, /proxy.downstream-proxy-dial-retries:0 Downstream proxy dial retries (default: 0) [%ESCOBAR_PROXY_DOWNSTREAM_PROXY_DIAL_RETRIES%] 102 | /proxy.ping-url: URL to ping anc check credentials validity (default: https://www.google.com/) [%ESCOBAR_PROXY_PING_URL%] 103 | /m, /proxy.mode: Escobar mode (default: auto) [%ESCOBAR_PROXY_MODE%] 104 | 105 | Downstream Proxy authentication: 106 | /u, /proxy.downstream-proxy-auth.user: Downstream Proxy user [%ESCOBAR_PROXY_DOWNSTREAM_PROXY_AUTH_USER%] 107 | /p, /proxy.downstream-proxy-auth.password: Downstream Proxy password [%ESCOBAR_PROXY_DOWNSTREAM_PROXY_AUTH_PASSWORD%] 108 | /k, /proxy.downstream-proxy-auth.keytab: Downstream Proxy path to keytab-file [%ESCOBAR_PROXY_DOWNSTREAM_PROXY_AUTH_KEYTAB%] 109 | 110 | Kerberos options: 111 | /proxy.kerberos.realm:EVIL.CORP Kerberos realm [%ESCOBAR_PROXY_KERBEROS_REALM%] 112 | /proxy.kerberos.kdc:kdc.evil.corp:88 Key Distribution Center (KDC) address [%ESCOBAR_PROXY_KERBEROS_KDC%] 113 | 114 | Server timeouts: 115 | /proxy.timeouts.server.read: HTTP server read timeout (default: 0s) [%ESCOBAR_PROXY_TIMEOUTS_SERVER_READ%] 116 | /proxy.timeouts.server.read-header: HTTP server read header timeout (default: 30s) [%ESCOBAR_PROXY_TIMEOUTS_SERVER_READ_HEADER%] 117 | /proxy.timeouts.server.write: HTTP server write timeout (default: 0s) [%ESCOBAR_PROXY_TIMEOUTS_SERVER_WRITE%] 118 | /proxy.timeouts.server.idle: HTTP server idle timeout (default: 1m) [%ESCOBAR_PROXY_TIMEOUTS_SERVER_IDLE%] 119 | 120 | Client timeouts: 121 | /proxy.timeouts.client.read: Client read timeout (default: 0s) [%ESCOBAR_PROXY_TIMEOUTS_CLIENT_READ%] 122 | /proxy.timeouts.client.write: Client write timeout (default: 0s) [%ESCOBAR_PROXY_TIMEOUTS_CLIENT_WRITE%] 123 | /proxy.timeouts.client.keepalive-period: Client keepalive period (default: 1m) [%ESCOBAR_PROXY_TIMEOUTS_CLIENT_KEEPALIVE_PERIOD%] 124 | 125 | Downstream Proxy timeouts: 126 | /proxy.timeouts.downstream.dial: Downstream proxy dial timeout (default: 10s) [%ESCOBAR_PROXY_TIMEOUTS_DOWNSTREAM_DIAL%] 127 | /proxy.timeouts.downstream.read: Downstream proxy read timeout (default: 0s) [%ESCOBAR_PROXY_TIMEOUTS_DOWNSTREAM_READ%] 128 | /proxy.timeouts.downstream.write: Downstream proxy write timeout (default: 0s) [%ESCOBAR_PROXY_TIMEOUTS_DOWNSTREAM_WRITE%] 129 | /proxy.timeouts.downstream.keepalive-period: Downstream proxy keepalive period (default: 1m) [%ESCOBAR_PROXY_TIMEOUTS_DOWNSTREAM_KEEPALIVE_PERIOD%] 130 | 131 | Static args: 132 | /static.addr: Static server address (default: localhost:3129) [%ESCOBAR_STATIC_ADDR%] 133 | 134 | Help Options: 135 | /? Show this help message 136 | /h, /help Show this help message 137 | ``` 138 | 139 | ### Keytab-file support 140 | Buy default I recommend to use `auto` mode that use Windows SSPI or Linux ccache. 141 | But you could also use `manual` mode to pass keytab-files instead of passing plain password. 142 | It's safer and less accessible. Below you can read about recommended setup. 143 | 144 | 1. Create service user `escobar` and lock it: 145 | 146 | ```bash 147 | # useradd -M escobar 148 | # usermod -L escobar 149 | ``` 150 | 2. Create `escobar` directory inside `/etc` and give `escobar` user proper rights: 151 | 152 | ```bash 153 | # mkdir /etc/escobar 154 | # chown escobar:escobar /etc/escobar 155 | # chmod 0700 /etc/escobar 156 | ``` 157 | 3. Create proper keytab-file by using `ktutil` utility: 158 | 159 | ```bash 160 | $ sudo -u escobar ktutil 161 | ktutil: add_entry -password -p ivanovii@EVIL.CORP -k 0 -e aes256-cts-hmac-sha1-96 162 | Password for ivanovii@EVIL.CORP: 163 | ktutil: write_kt /etc/escobar/ivanovii.keytab 164 | ktutil: exit 165 | ``` 166 | 167 | #### Keytab-file troubleshooting 168 | * Check rights: 169 | * Directory should have `0700`. 170 | * Keytab-file itself `0600`. 171 | * Both directory and keytab should be owned by user who execute software. 172 | * Check principal name. Usually it's your username and realm: `username@REALM.COM` 173 | * Check KVNO, it could be invalid. Everytime you change your password, KVNO updates by rule `Current KVNO + 1`. 174 | * Check encryption type. In example guide above I showed how to generate keytab-file with `aes256-cts-hmac-sha1-96`. 175 | But in your case it could be another cipher. 176 | 177 | In case of principal name, KVNO and encryption type program should print what does it looking for. 178 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= 2 | bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= 3 | github.com/L11R/go-spnego v0.0.0-20220327233043-e75f5ec4d8b1 h1:YTC+WGMRw+doS7joYQJIQ2pDAs1TijPh8IczMCizOvg= 4 | github.com/L11R/go-spnego v0.0.0-20220327233043-e75f5ec4d8b1/go.mod h1:FbXVSYp/xm4IqrCz9/WZkAWmRG5/4JCPQkGchErqpJ4= 5 | github.com/L11R/httputil v0.0.0-20220615134631-4431dfe56a3f h1:3kFf7gFKhnoFRY+1FaXiqWMTxYuDsMesTQ6bHMiDAsM= 6 | github.com/L11R/httputil v0.0.0-20220615134631-4431dfe56a3f/go.mod h1:BfM/NB9qaF9HPs6XyMzCJ5Ip6JPrcm9tsTiBzrW7nqQ= 7 | github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= 8 | github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8= 13 | github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 14 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= 15 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 16 | github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0= 17 | github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 18 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 19 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 20 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 21 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 22 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 23 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 24 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 25 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 26 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 27 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 28 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 29 | github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 30 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 31 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 32 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 33 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 34 | github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= 35 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 36 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 37 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 38 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 39 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 40 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 41 | github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= 42 | github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= 43 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 46 | github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w= 47 | github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y= 48 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 49 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 50 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 51 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 52 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 53 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 54 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 55 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 56 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 57 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 58 | github.com/undefinedlabs/go-mpatch v1.0.7 h1:943FMskd9oqfbZV0qRVKOUsXQhTLXL0bQTVbQSpzmBs= 59 | github.com/undefinedlabs/go-mpatch v1.0.7/go.mod h1:TyJZDQ/5AgyN7FSLiBJ8RO9u2c6wbtRvK827b6AVqY4= 60 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 61 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 62 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 63 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 64 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 65 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 66 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 67 | golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 68 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 69 | golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 70 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 71 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 72 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 73 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 74 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 75 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 76 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 77 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 78 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 79 | golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 80 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 81 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 82 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 83 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 84 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 85 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 86 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 88 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 99 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 100 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 101 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 102 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 103 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 104 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 105 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 107 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 108 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 109 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 112 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 113 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 114 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 115 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 116 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 117 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 118 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 119 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 120 | -------------------------------------------------------------------------------- /internal/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Savely Krasovsky. All rights reserved. 2 | // Use of this source code is governed by the Apache 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "bufio" 9 | "context" 10 | "crypto/tls" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "io/ioutil" 15 | "net" 16 | "net/http" 17 | "net/url" 18 | "time" 19 | 20 | "github.com/L11R/httputil" 21 | 22 | "github.com/jcmturner/gokrb5/v8/client" 23 | "go.uber.org/zap" 24 | ) 25 | 26 | const ( 27 | LogEntryCtx = "log_entry" 28 | HeaderProxyAuthorization = "Proxy-Authorization" 29 | ) 30 | 31 | type Proxy struct { 32 | logger *zap.Logger 33 | config *Config 34 | krb5cl *client.Client 35 | server *http.Server 36 | httpProxy *httputil.ReverseProxy 37 | } 38 | 39 | // NewProxy returns Proxy instance 40 | func NewProxy(logger *zap.Logger, config *Config, krb5cl *client.Client) *Proxy { 41 | fp := httputil.NewForwardingProxy() 42 | fp.ErrorHandler = httpErrorHandler 43 | 44 | p := &Proxy{ 45 | logger: logger, 46 | config: config, 47 | krb5cl: krb5cl, 48 | httpProxy: fp, 49 | } 50 | 51 | p.httpProxy.ErrorLog = zap.NewStdLog(logger) 52 | p.server = &http.Server{ 53 | Addr: config.Addr.String(), 54 | Handler: p, 55 | // Disable HTTP/2. 56 | TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), 57 | // Timeouts 58 | ReadTimeout: config.Timeouts.Server.ReadTimeout, 59 | ReadHeaderTimeout: config.Timeouts.Server.ReadHeaderTimeout, 60 | WriteTimeout: config.Timeouts.Server.WriteTimeout, 61 | IdleTimeout: config.Timeouts.Server.IdleTimeout, 62 | } 63 | 64 | return p 65 | } 66 | 67 | // CheckAuth checks auth against Ping URL; should be called after starting proxy server itself 68 | func (p *Proxy) CheckAuth() (bool, error) { 69 | u, err := url.Parse("http://" + p.config.Addr.String()) 70 | if err != nil { 71 | return false, fmt.Errorf("invalid proxy url: %w", err) 72 | } 73 | 74 | // We need http client with custom transport 75 | httpClient := http.DefaultClient 76 | 77 | tr := http.DefaultTransport 78 | // Pass our newly deployed local proxy 79 | tr.(*http.Transport).Proxy = http.ProxyURL(u) 80 | // We check it against corporate proxy, so it usually use MITM 81 | tr.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 82 | 83 | httpClient.Transport = tr 84 | 85 | req, err := http.NewRequest("GET", p.config.PingURL.String(), nil) 86 | if err != nil { 87 | return false, fmt.Errorf("cannot create request: %w", err) 88 | } 89 | 90 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 91 | defer cancel() 92 | req = req.WithContext(ctx) 93 | 94 | if err := p.setProxyAuthorizationHeader(req); err != nil { 95 | return false, fmt.Errorf("cannot set authorization header: %w", err) 96 | } 97 | 98 | repeat := true 99 | 100 | Again: 101 | resp, err := httpClient.Do(req) 102 | if err != nil { 103 | return false, fmt.Errorf("cannot do request: %w", err) 104 | } 105 | //noinspection ALL 106 | defer resp.Body.Close() 107 | 108 | if _, err := ioutil.ReadAll(resp.Body); err != nil { 109 | return false, fmt.Errorf("cannot read body: %w", err) 110 | } 111 | 112 | if resp.StatusCode == http.StatusProxyAuthRequired && repeat { 113 | repeat = false 114 | goto Again 115 | } 116 | 117 | if resp.StatusCode == http.StatusOK { 118 | return true, nil 119 | } 120 | 121 | return false, nil 122 | } 123 | 124 | func (p *Proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 125 | logger := p.logger.With( 126 | zap.String("http_proto", req.Proto), 127 | zap.String("http_method", req.Method), 128 | zap.String("user_agent", req.UserAgent()), 129 | zap.String("uri", req.RequestURI), 130 | ) 131 | // nolint:staticcheck 132 | req = req.WithContext(context.WithValue(context.Background(), LogEntryCtx, logger)) 133 | 134 | defer func() { 135 | if r := recover(); r != nil { 136 | if err, ok := r.(error); ok { 137 | logger.Error("Panic recovered", zap.Error(err)) 138 | } 139 | 140 | rw.WriteHeader(http.StatusInternalServerError) 141 | return 142 | } 143 | }() 144 | 145 | logger.Debug("Request started") 146 | 147 | if req.URL.Scheme == "http" { 148 | p.http(rw, req) 149 | } else { 150 | p.https(rw, req) 151 | } 152 | 153 | logger.Debug("Request completed") 154 | } 155 | 156 | func httpErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { 157 | logger := req.Context().Value(LogEntryCtx).(*zap.Logger) 158 | 159 | if errors.Is(err, context.Canceled) { 160 | logger.Debug("http: proxy client disconnected") 161 | return 162 | } 163 | 164 | logger.Error("http: proxy error", zap.Error(err)) 165 | rw.WriteHeader(http.StatusBadGateway) 166 | } 167 | 168 | func (p *Proxy) http(rw http.ResponseWriter, req *http.Request) { 169 | tr := http.DefaultTransport 170 | tr.(*http.Transport).Proxy = http.ProxyURL(p.config.DownstreamProxyURL) 171 | 172 | if err := p.setProxyAuthorizationHeader(req); err != nil { 173 | httpErrorHandler(rw, req, fmt.Errorf("cannot set authorization header: %w", err)) 174 | return 175 | } 176 | 177 | p.httpProxy.ServeHTTP(rw, req) 178 | } 179 | 180 | func httpsErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { 181 | logger := req.Context().Value(LogEntryCtx).(*zap.Logger) 182 | 183 | logger.Error("https: proxy error", zap.Error(err)) 184 | rw.WriteHeader(http.StatusBadGateway) 185 | } 186 | 187 | func httpsErrorHijackedHandler(brw *bufio.ReadWriter, req *http.Request, err error) { 188 | logger := req.Context().Value(LogEntryCtx).(*zap.Logger) 189 | 190 | logger.Error("https: proxy error", zap.Error(err)) 191 | 192 | resp := newResponse(http.StatusBadGateway, nil, req) 193 | if err := resp.Write(brw); err != nil { 194 | logger.Error("Cannot write response", zap.Error(err)) 195 | } 196 | if err := brw.Flush(); err != nil { 197 | logger.Error("Cannot flush writer", zap.Error(err)) 198 | } 199 | } 200 | 201 | func (p *Proxy) https(rw http.ResponseWriter, req *http.Request) { 202 | logger := req.Context().Value(LogEntryCtx).(*zap.Logger) 203 | 204 | // Check that we are handling CONNECT 205 | if req.Method != http.MethodConnect { 206 | http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 207 | return 208 | } 209 | 210 | // Take control of the connection 211 | hj, ok := rw.(http.Hijacker) 212 | if !ok { 213 | httpsErrorHandler(rw, req, fmt.Errorf("hijacking is not supported")) 214 | return 215 | } 216 | 217 | conn, brw, err := hj.Hijack() 218 | if err != nil { 219 | httpsErrorHandler(rw, req, fmt.Errorf("hijack failed: %w", err)) 220 | return 221 | } 222 | defer func() { 223 | if err := conn.Close(); err != nil { 224 | logger.Error("Cannot close connection", zap.Error(err)) 225 | } 226 | }() 227 | 228 | // Set Keep-Alive 229 | if tconn, ok := conn.(*net.TCPConn); ok { 230 | if err := tconn.SetKeepAlive(true); err != nil { 231 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot turn on keep-alive: %w", err)) 232 | return 233 | } 234 | if err := tconn.SetKeepAlivePeriod(p.config.Timeouts.Client.KeepAlivePeriod); err != nil { 235 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot set keep-alive period: %w", err)) 236 | return 237 | } 238 | } 239 | 240 | // Set client connection timeouts 241 | now := time.Now() 242 | if p.config.Timeouts.Client.ReadTimeout.Nanoseconds() != 0 { 243 | if err := conn.SetReadDeadline(now.Add(p.config.Timeouts.Client.ReadTimeout)); err != nil { 244 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot set read timeout for connection with client: %w", err)) 245 | return 246 | } 247 | } 248 | if p.config.Timeouts.Client.WriteTimeout.Nanoseconds() != 0 { 249 | if err := conn.SetWriteDeadline(now.Add(p.config.Timeouts.Client.WriteTimeout)); err != nil { 250 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot set write timeout for connection with client: %w", err)) 251 | return 252 | } 253 | } 254 | 255 | p.connectAndCopy(conn, brw, req, false) 256 | } 257 | 258 | // connectAndCopy connects to downstream proxy, authenticates if there is a need and copies traffic between connections 259 | func (p *Proxy) connectAndCopy(conn net.Conn, brw *bufio.ReadWriter, req *http.Request, reconnected bool) { 260 | logger := req.Context().Value(LogEntryCtx).(*zap.Logger) 261 | 262 | retries := 0 263 | ProxyDialRetry: 264 | // Open connection with downstream proxy 265 | pconn, err := net.DialTimeout("tcp", p.config.DownstreamProxyURL.Host, p.config.Timeouts.DownstreamProxy.DialTimeout) 266 | if err != nil { 267 | if retries < p.config.DownstreamProxyDialRetries { 268 | logger.Error("Connection to downstream proxy failed.", zap.Error(err)) 269 | retries++ 270 | time.Sleep(1 * time.Second) 271 | logger.Debug("Downstream proxy dial retry.", zap.Int("retries", retries)) 272 | 273 | goto ProxyDialRetry 274 | } 275 | 276 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot connect to downstream proxy: %w", err)) 277 | return 278 | } 279 | defer func() { 280 | if err := pconn.Close(); err != nil { 281 | logger.Error("Cannot close connection", zap.Error(err)) 282 | } 283 | }() 284 | 285 | // Set Keep-Alive 286 | if tconn, ok := pconn.(*net.TCPConn); ok { 287 | if err := tconn.SetKeepAlive(true); err != nil { 288 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot turn on keep-alive: %w", err)) 289 | return 290 | } 291 | if err := tconn.SetKeepAlivePeriod(p.config.Timeouts.DownstreamProxy.KeepAlivePeriod); err != nil { 292 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot set keep-alive period: %w", err)) 293 | return 294 | } 295 | } 296 | 297 | // Set Downstream Proxy connection timeouts 298 | now := time.Now() 299 | if p.config.Timeouts.DownstreamProxy.ReadTimeout.Nanoseconds() != 0 { 300 | if err := pconn.SetReadDeadline(now.Add(p.config.Timeouts.DownstreamProxy.ReadTimeout)); err != nil { 301 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot set read timeout for connection with downstream proxy: %w", err)) 302 | return 303 | } 304 | } 305 | if p.config.Timeouts.DownstreamProxy.WriteTimeout.Nanoseconds() != 0 { 306 | if err := pconn.SetWriteDeadline(now.Add(p.config.Timeouts.DownstreamProxy.WriteTimeout)); err != nil { 307 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot set write timeout for connection with downstream proxy: %w", err)) 308 | return 309 | } 310 | } 311 | 312 | pbw := bufio.NewWriter(pconn) 313 | pbr := bufio.NewReader(pconn) 314 | 315 | // Write client's request into proxy connection 316 | if err := req.Write(pbw); err != nil { 317 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot write request into proxy connection: %w", err)) 318 | return 319 | } 320 | if err := pbw.Flush(); err != nil { 321 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot flush writer to commit request into proxy connection: %w", err)) 322 | return 323 | } 324 | 325 | // Read response from body, usually it's just 407 Proxy Authentication Required 326 | resp, err := http.ReadResponse(pbr, req) 327 | if err != nil { 328 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot read response from proxy connection: %w", err)) 329 | return 330 | } 331 | 332 | switch resp.StatusCode { 333 | // Proxy authentication required, we have to pass Proxy-Authorization header 334 | case http.StatusProxyAuthRequired: 335 | // Close the body, no need to read it, there is only HTML template with Proxy warning 336 | //noinspection ALL 337 | resp.Body.Close() 338 | 339 | // Set Proxy-Authorization header 340 | if err := p.setProxyAuthorizationHeader(req); err != nil { 341 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot set authrorization header: %w", err)) 342 | return 343 | } 344 | 345 | // Write request into proxy connection again, now with proper auth 346 | if err := req.Write(pbw); err != nil { 347 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot write request into proxy connection: %w", err)) 348 | return 349 | } 350 | if err := pbw.Flush(); err != nil { 351 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot flush writer to commit request into proxy connection: %w", err)) 352 | return 353 | } 354 | 355 | // Read proxy response again, hope user credentials are valid and proxy returned 200 356 | resp, err = http.ReadResponse(pbr, req) 357 | if err != nil { 358 | // Some proxies drop connection after responding 407 359 | var target *net.OpError 360 | if (errors.As(err, &target) || errors.Is(err, io.ErrUnexpectedEOF)) && !reconnected { 361 | // Reconnection could be tried only once to prevent infinity loop; 362 | // Proxy-Authorization header is already set, so we can try again; 363 | p.connectAndCopy(conn, brw, req, true) 364 | return 365 | } 366 | 367 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot read response from proxy connection: %w", err)) 368 | return 369 | } 370 | if resp.StatusCode == http.StatusOK { 371 | resp.Body = nil 372 | } else { 373 | logger.Warn("Proxy did NOT return 200 Connection established", zap.Int("proxy_resp_code", resp.StatusCode)) 374 | } 375 | 376 | // Return this response to client 377 | if err := resp.Write(brw); err != nil { 378 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot write response from proxy into client connection: %w", err)) 379 | return 380 | } 381 | if err := brw.Flush(); err != nil { 382 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot flush writer to commit response from proxy into client connection: %w", err)) 383 | return 384 | } 385 | // 200 Connection established, we can immediately start data transfer 386 | case http.StatusOK: 387 | // Set body to nil, otherwise we will get deadlock 388 | resp.Body = nil 389 | 390 | // Return this response to client 391 | if err := resp.Write(brw); err != nil { 392 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot write response from proxy into client connection: %w", err)) 393 | return 394 | } 395 | if err := brw.Flush(); err != nil { 396 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("cannot flush writer to commit response from proxy into client connection: %w", err)) 397 | return 398 | } 399 | default: 400 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("unknown code recieved: %d", resp.StatusCode)) 401 | return 402 | } 403 | 404 | // Start traffic copying inside newly created tunnel 405 | errc := make(chan error, 1) 406 | cc := connectCopier{ 407 | logger: logger, 408 | client: conn, 409 | backend: pconn, 410 | } 411 | go cc.copyToBackend(errc) 412 | go cc.copyFromBackend(errc) 413 | 414 | logger.Debug("CONNECT tunnel opened") 415 | defer logger.Debug("CONNECT tunnel closed") 416 | 417 | err = <-errc 418 | if err == nil { 419 | err = <-errc 420 | } 421 | 422 | if err != nil { 423 | httpsErrorHijackedHandler(brw, req, fmt.Errorf("traffic copying inside tunnel failed: %w", err)) 424 | return 425 | } 426 | 427 | logger.Debug("Traffic copied successfully") 428 | } 429 | 430 | func (p *Proxy) Listen() (net.Listener, error) { 431 | p.logger.Info("Listening socket", zap.String("address", p.config.Addr.String())) 432 | 433 | l, err := net.Listen("tcp", p.config.Addr.String()) 434 | if err != nil { 435 | p.logger.Error("Error while listening!", zap.Error(err)) 436 | return nil, err 437 | } 438 | 439 | return l, nil 440 | } 441 | 442 | // Serve serves HTTP requests. 443 | func (p *Proxy) Serve(l net.Listener) error { 444 | p.logger.Info("Serving HTTP requests", zap.String("address", p.config.Addr.String())) 445 | 446 | if err := p.server.Serve(l); err != nil { 447 | if !errors.Is(err, http.ErrServerClosed) { 448 | p.logger.Error("Error while serving HTTP requests!", zap.Error(err)) 449 | return err 450 | } 451 | } 452 | 453 | return nil 454 | } 455 | 456 | // Shutdown shuts down the HTTP server. 457 | func (p *Proxy) Shutdown(ctx context.Context) error { 458 | if err := p.server.Shutdown(ctx); err != nil { 459 | p.logger.Error("Error shutting down HTTP server!", zap.Error(err)) 460 | return err 461 | } 462 | 463 | return nil 464 | } 465 | --------------------------------------------------------------------------------