From 81f734f95088934c2fb9f1cdbdebbbad3314d0f9 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 15 Aug 2023 15:51:30 -0400 Subject: [PATCH] Add support for http connect proxying to tls connections. Fixes #54 --- address.go | 109 ++++++++++++++++++++++++++++++++++++++- go.mod | 6 +-- go.sum | 12 +++-- proxies/http_connect.go | 110 ++++++++++++++++++++++++++++++++++++++++ tls/address.go | 8 ++- tls/dialer.go | 45 +++++++++++++--- 6 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 proxies/http_connect.go diff --git a/address.go b/address.go index 336fa93..e6f8e54 100644 --- a/address.go +++ b/address.go @@ -21,11 +21,18 @@ import ( "github.com/openziti/identity" "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "golang.org/x/net/proxy" "io" "net" "time" ) +const ( + KeyProxy = "proxy" + KeyProtocol = "protocol" + KeyCachedProxyConfiguration = "cachedProxyConfiguration" +) + type Configuration map[interface{}]interface{} // Protocols returns supported or requested application protocols (used for ALPN support) @@ -34,7 +41,7 @@ func (self Configuration) Protocols() []string { return nil } - p, found := self["protocol"] + p, found := self[KeyProtocol] if found { switch v := p.(type) { case string: @@ -48,6 +55,106 @@ func (self Configuration) Protocols() []string { return nil } +func (self Configuration) GetProxyConfiguration() (*ProxyConfiguration, error) { + if self == nil { + return nil, nil + } + + if val, found := self[KeyCachedProxyConfiguration]; found { + return val.(*ProxyConfiguration), nil + } + + val, found := self[KeyProxy] + if !found { + return nil, nil + } + + cfg, ok := val.(map[interface{}]interface{}) + if !ok { + return nil, errors.New("invalid proxy configuration value, should be map") + } + + result, err := LoadProxyConfiguration(cfg) + if err != nil { + return nil, err + } + + self[KeyCachedProxyConfiguration] = result + + return result, nil +} + +type ProxyType string + +const ( + ProxyTypeNone ProxyType = "none" + ProxyTypeHttpConnect ProxyType = "http" +) + +type ProxyConfiguration struct { + Type ProxyType + Address string + Auth *proxy.Auth +} + +func LoadProxyConfiguration(cfg map[interface{}]interface{}) (*ProxyConfiguration, error) { + val, found := cfg["type"] + if !found { + return nil, errors.New("proxy configuration does not specify proxy type") + } + + proxyType, ok := val.(string) + if !ok { + return nil, errors.New("proxy type must be a string") + } + + if proxyType == string(ProxyTypeNone) { + return &ProxyConfiguration{ + Type: ProxyTypeNone, + }, nil + } + + result := &ProxyConfiguration{} + + switch proxyType { + case string(ProxyTypeHttpConnect): + result.Type = ProxyTypeHttpConnect + default: + return nil, errors.Errorf("invalid proxy type %s", proxyType) + } + + val, found = cfg["address"] + if !found { + return nil, errors.Errorf("no address specified for %s proxy", string(result.Type)) + } + + if addr, ok := val.(string); !ok { + return nil, errors.Errorf("invalid value for %s proxy address [%v], must be string", string(result.Type), val) + } else { + result.Address = addr + } + + if val, found = cfg["username"]; found { + if username, ok := val.(string); ok { + result.Auth = &proxy.Auth{ + User: username, + } + } else { + return nil, errors.Errorf("invalid value for %s proxy username [%v], must be string", string(result.Type), val) + } + + if val, found = cfg["password"]; found { + if password, ok := val.(string); ok { + result.Auth.Password = password + } else { + return nil, errors.Errorf("invalid value for %s proxy password [%v], must be string", string(result.Type), val) + } + } + } + + return result, nil +} + // Address implements the functionality provided by a generic "address". type Address interface { Dial(name string, i *identity.TokenId, timeout time.Duration, tcfg Configuration) (Conn, error) diff --git a/go.mod b/go.mod index 5fa7adf..b0278e0 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/klauspost/compress v1.10.3 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/nxadm/tail v1.4.8 // indirect @@ -35,9 +35,9 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/parallaxsecond/parsec-client-go v0.0.0-20220111122524-cb78842db373 // indirect + github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/text v0.12.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4cf8b3e..0503c6c 100644 --- a/go.sum +++ b/go.sum @@ -300,8 +300,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= @@ -353,8 +354,8 @@ github.com/openziti/foundation/v2 v2.0.29 h1:E63p5/esqOJ/OSMePR3fKYHb3Wq2BR4PLkD github.com/openziti/foundation/v2 v2.0.29/go.mod h1:MpXSCSn4MABvtIXzfTBFqhK5pNsNXHWnR8xxVrfxn0g= github.com/openziti/identity v1.0.60 h1:6gvBXY9J6F7SbuksdxsUA1t1WmtsFfY61Oqm/00ijGU= github.com/openziti/identity v1.0.60/go.mod h1:pUfQ1Rf6TJvpBULXKPAO4014Qd6g+uf6V/vqjUscipU= -github.com/parallaxsecond/parsec-client-go v0.0.0-20220111122524-cb78842db373 h1:CUvH4JL/8OVy023LMER3dB/MerNQ6OIz4QV3E/JQ3UY= -github.com/parallaxsecond/parsec-client-go v0.0.0-20220111122524-cb78842db373/go.mod h1:gLH27qo/dvMhLTVVyMELpe3Tut7sOfkiDg7ZpeqKwsw= +github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 h1:mOvehYivJ4Aqu2CPe3D3lv8jhqOI9/1o0THxJHBE0qw= +github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9/go.mod h1:gLH27qo/dvMhLTVVyMELpe3Tut7sOfkiDg7ZpeqKwsw= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -698,6 +699,7 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -901,8 +903,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/proxies/http_connect.go b/proxies/http_connect.go new file mode 100644 index 0000000..3676711 --- /dev/null +++ b/proxies/http_connect.go @@ -0,0 +1,110 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package proxies + +import ( + "bufio" + "context" + "github.com/michaelquigley/pfxlog" + "github.com/pkg/errors" + "golang.org/x/net/proxy" + "io" + "net" + "net/http" + "net/url" + "time" +) + +func NewHttpConnectProxyDialer(addr string, auth *proxy.Auth, timeout time.Duration) *HttpConnectProxyDialer { + return &HttpConnectProxyDialer{ + address: addr, + auth: auth, + timeout: timeout, + } +} + +type HttpConnectProxyDialer struct { + address string + auth *proxy.Auth + timeout time.Duration +} + +func (self *HttpConnectProxyDialer) Dial(network, addr string) (net.Conn, error) { + c, err := net.Dial(network, self.address) + if err != nil { + return nil, errors.Wrapf(err, "unable to connect to proxy server at %s", self.address) + } + + if err = self.Connect(c, addr); err != nil { + if closeErr := c.Close(); closeErr != nil { + pfxlog.Logger().WithError(closeErr).Error("failed to close connection to proxy after connect error") + } + return nil, err + } + + return c, nil +} + +func (self *HttpConnectProxyDialer) Connect(c net.Conn, addr string) error { + log := pfxlog.Logger() + + log.Infof("create connect request to %s", addr) + + ctx := context.Background() + if self.timeout > 0 { + timeoutCtx, cancelF := context.WithTimeout(ctx, self.timeout) + defer cancelF() + ctx = timeoutCtx + } + + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Host: addr}, + Host: addr, + Header: http.Header{}, + Close: false, + } + req = req.WithContext(ctx) + if self.auth != nil { + req.SetBasicAuth(self.auth.User, self.auth.Password) + } + req.Header.Set("User-Agent", "ziti-transport") + + log.Info("writing request to wire") + if err := req.Write(c); err != nil { + return errors.Wrapf(err, "unable to send connect request to proxy server at %s", self.address) + } + + log.Info("reading response from wire") + resp, err := http.ReadResponse(bufio.NewReader(c), req) + if err != nil { + return errors.Wrapf(err, "unable to read response to connect request to proxy server at %s", self.address) + } + + defer func() { + log.Info("closing resp body") + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + log.Errorf("proxy returned: %s", string(respBody)) + return errors.Errorf("received %v instead of 200 OK in response to connect request to proxy server at %s", resp.StatusCode, self.address) + } + + return nil +} diff --git a/tls/address.go b/tls/address.go index 16d39d0..93e44ed 100644 --- a/tls/address.go +++ b/tls/address.go @@ -17,10 +17,10 @@ package tls import ( - "errors" "fmt" "github.com/openziti/identity" "github.com/openziti/transport/v2" + "github.com/pkg/errors" "io" "strconv" "strings" @@ -41,7 +41,11 @@ func (a address) Dial(name string, i *identity.TokenId, timeout time.Duration, c } func (a address) DialWithLocalBinding(name string, localBinding string, i *identity.TokenId, timeout time.Duration, tcfg transport.Configuration) (transport.Conn, error) { - return DialWithLocalBinding(a.bindableAddress(), name, localBinding, i, timeout, tcfg.Protocols()...) + proxyConfig, err := tcfg.GetProxyConfiguration() + if err != nil { + return nil, errors.Wrapf(err, "unable to get proxy configuration") + } + return DialWithLocalBinding(a, name, localBinding, i, timeout, proxyConfig, tcfg.Protocols()...) } func (a address) Listen(name string, i *identity.TokenId, acceptF func(transport.Conn), tcfg transport.Configuration) (io.Closer, error) { diff --git a/tls/dialer.go b/tls/dialer.go index 79338a0..3903ef5 100644 --- a/tls/dialer.go +++ b/tls/dialer.go @@ -21,7 +21,8 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" "github.com/openziti/transport/v2" - log "github.com/sirupsen/logrus" + "github.com/openziti/transport/v2/proxies" + "github.com/pkg/errors" "net" "time" ) @@ -48,25 +49,53 @@ func Dial(destination, name string, i *identity.TokenId, timeout time.Duration, }, nil } -func DialWithLocalBinding(destination, name, localBinding string, i *identity.TokenId, timeout time.Duration, protocols ...string) (transport.Conn, error) { +func DialWithLocalBinding(a address, name, localBinding string, i *identity.TokenId, timeout time.Duration, proxyConf *transport.ProxyConfiguration, protocols ...string) (transport.Conn, error) { + destination := a.bindableAddress() dialer, err := transport.NewDialerWithLocalBinding("tcp", timeout, localBinding) - if err != nil { return nil, err } + log := pfxlog.Logger().WithField("dest", destination) + tlsCfg := i.ClientTLSConfig() + tlsCfg.ServerName = a.hostname if len(protocols) > 0 { tlsCfg = tlsCfg.Clone() tlsCfg.NextProtos = append(tlsCfg.NextProtos, protocols...) } - socket, err := tls.DialWithDialer(dialer, "tcp", destination, tlsCfg) - if err != nil { - return nil, err + var tlsConn *tls.Conn + + if proxyConf != nil && proxyConf.Type != transport.ProxyTypeNone { + if proxyConf.Type == transport.ProxyTypeHttpConnect { + log.Infof("using http connect proxy at %s", proxyConf.Address) + conn, err := dialer.Dial("tcp", proxyConf.Address) + if err != nil { + return nil, err + } + + log.Info("sending HTTP CONNECT") + proxyDialer := proxies.NewHttpConnectProxyDialer(destination, proxyConf.Auth, timeout) + if err = proxyDialer.Connect(conn, destination); err != nil { + if closeErr := conn.Close(); closeErr != nil { + log.WithError(closeErr).Error("unable to close underlying connection after http connect error") + } + return nil, err + } + + tlsConn = tls.Client(conn, tlsCfg) + } else { + return nil, errors.Errorf("unsupported proxy type %s", string(proxyConf.Type)) + } + } else { + tlsConn, err = tls.DialWithDialer(dialer, "tcp", destination, tlsCfg) + if err != nil { + return nil, err + } } - log.Debugf("server provided [%d] certificates", len(socket.ConnectionState().PeerCertificates)) + log.Debugf("server provided [%d] certificates", len(tlsConn.ConnectionState().PeerCertificates)) return &Connection{ detail: &transport.ConnectionDetail{ @@ -74,6 +103,6 @@ func DialWithLocalBinding(destination, name, localBinding string, i *identity.To InBound: false, Name: name, }, - Conn: socket, + Conn: tlsConn, }, nil }