From 767644a95f8860ef1935e978f5964afa226a4dba Mon Sep 17 00:00:00 2001 From: Abhijit Herekar Date: Mon, 13 Jul 2020 21:25:31 -0700 Subject: [PATCH] ManipHttp: Add client TCP_USERTIMEOUT option to the socket. (#121) * ManipHttp: Add client TCP_USERTIMEOUT option to the socket. * Turn off the socketOption by default. * Fix review comments. * Fix lint. * Fix UT. * Add UT. * Add random port and fix review. * Fix typos. --- go.mod | 1 + go.sum | 1 + maniphttp/internal/syscall/syscall_linux.go | 33 +++++ .../internal/syscall/syscall_nonlinux.go | 15 +++ maniphttp/manipulator.go | 17 +-- maniphttp/manipulator_test.go | 126 ++++++++++++++++++ maniphttp/options.go | 9 ++ maniphttp/options_test.go | 8 ++ maniphttp/utils.go | 4 +- 9 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 maniphttp/internal/syscall/syscall_linux.go create mode 100644 maniphttp/internal/syscall/syscall_nonlinux.go diff --git a/go.mod b/go.mod index 8d12d06f..60638e72 100644 --- a/go.mod +++ b/go.mod @@ -19,4 +19,5 @@ require ( go.uber.org/zap v1.14.0 golang.org/x/lint v0.0.0-20200130185559-910be7a94367 // indirect golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e + golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae ) diff --git a/go.sum b/go.sum index 2ea2fd1b..8207bca1 100644 --- a/go.sum +++ b/go.sum @@ -242,6 +242,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/maniphttp/internal/syscall/syscall_linux.go b/maniphttp/internal/syscall/syscall_linux.go new file mode 100644 index 00000000..60cec62c --- /dev/null +++ b/maniphttp/internal/syscall/syscall_linux.go @@ -0,0 +1,33 @@ +// +build linux + +package syscall + +// package to set to the low-level/OS settings + +import ( + "os" + "syscall" + "time" + + "golang.org/x/sys/unix" +) + +// MakeDialerControlFunc creates a custom control for the dailer +func MakeDialerControlFunc(t time.Duration) func(string, string, syscall.RawConn) error { + // return if the tcpUserTimeout is not set. + if t == 0 { + return nil + } + + return func(network, address string, c syscall.RawConn) error { + var sysErr error + err := c.Control(func(fd uintptr) { + sysErr = syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, unix.TCP_USER_TIMEOUT, + int(t.Milliseconds())) + }) + if sysErr != nil { + return os.NewSyscallError("setsockopt", sysErr) + } + return err + } +} diff --git a/maniphttp/internal/syscall/syscall_nonlinux.go b/maniphttp/internal/syscall/syscall_nonlinux.go new file mode 100644 index 00000000..1aa772d6 --- /dev/null +++ b/maniphttp/internal/syscall/syscall_nonlinux.go @@ -0,0 +1,15 @@ +// +build !linux + +package syscall + +import ( + "syscall" + "time" +) + +// package to set to the low-level/OS settings + +// MakeDialerControlFunc creates a custom control for the dailer +func MakeDialerControlFunc(d time.Duration) func(string, string, syscall.RawConn) error { + return nil +} diff --git a/maniphttp/manipulator.go b/maniphttp/manipulator.go index 45cb0066..8bd39048 100644 --- a/maniphttp/manipulator.go +++ b/maniphttp/manipulator.go @@ -64,13 +64,14 @@ type httpManipulator struct { tokenCookieKey string // optionnable - ctx context.Context - client *http.Client - tlsConfig *tls.Config - tokenManager manipulate.TokenManager - globalHeaders http.Header - transport *http.Transport - encoding elemental.EncodingType + ctx context.Context + client *http.Client + tlsConfig *tls.Config + tokenManager manipulate.TokenManager + globalHeaders http.Header + transport *http.Transport + encoding elemental.EncodingType + tcpUserTimeout time.Duration } // New returns a maniphttp.Manipulator configured according to the given suite of Option. @@ -103,7 +104,7 @@ func New(ctx context.Context, url string, options ...Option) (manipulate.Manipul if m.transport == nil { - m.transport, m.url = getDefaultHTTPTransport(url, m.disableCompression) + m.transport, m.url = getDefaultHTTPTransport(url, m.disableCompression, m.tcpUserTimeout) if m.tlsConfig == nil { m.tlsConfig = getDefaultTLSConfig() diff --git a/maniphttp/manipulator_test.go b/maniphttp/manipulator_test.go index e8b43b58..bc521f02 100644 --- a/maniphttp/manipulator_test.go +++ b/maniphttp/manipulator_test.go @@ -15,9 +15,11 @@ import ( "context" "crypto/tls" "fmt" + "net" "net/http" "net/http/httptest" "sync/atomic" + "syscall" "testing" "time" @@ -27,8 +29,10 @@ import ( "go.aporeto.io/manipulate" "go.aporeto.io/manipulate/internal/idempotency" "go.aporeto.io/manipulate/internal/tracing" + internalsyscall "go.aporeto.io/manipulate/maniphttp/internal/syscall" "go.aporeto.io/manipulate/maniptest" "golang.org/x/sync/errgroup" + "golang.org/x/sys/unix" ) func TestHTTP_New(t *testing.T) { @@ -59,6 +63,10 @@ func TestHTTP_New(t *testing.T) { So(m.namespace, ShouldEqual, "myns") }) + Convey("Then the control dialer should be nil", func() { + So(m.tcpUserTimeout, ShouldEqual, 0) + }) + Convey("Then the it should implement Manipulator interface", func() { var i interface{} = m @@ -94,6 +102,7 @@ func TestHTTP_New(t *testing.T) { "http://url.com/", OptionHTTPTransport(transport), ) + m := mm.(*httpManipulator) Convey("Then the tls config is correct", func() { @@ -174,6 +183,123 @@ func TestHTTP_New(t *testing.T) { }) } +func TestHTTP_TCPUserTimeout(t *testing.T) { + Convey("When I create a simple manipulator with custom transport, with TCP option", t, func() { + dialer := (&net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 30 * time.Second, + Control: internalsyscall.MakeDialerControlFunc(30 * time.Second), + }).DialContext + + transport := &http.Transport{ + DialContext: dialer, + } + transport.TLSClientConfig = &tls.Config{} + + mm, _ := New( + context.Background(), + "http://url.com/", + OptionHTTPTransport(transport), + OptionTCPUserTimeout(40*time.Second), + ) + + m := mm.(*httpManipulator) + + Convey("Then the tls config is correct", func() { + So(m.tlsConfig, ShouldEqual, transport.TLSClientConfig) + }) + Convey("Then the dialer is correct", func() { + l, err := net.Listen("tcp", ":0") + So(err, ShouldBeNil) + + opt := -1 + dctx := m.client.Transport.(*http.Transport).DialContext + So(dctx, ShouldNotBeNil) + conn, err := dctx(context.TODO(), "tcp", l.Addr().String()) + So(err, ShouldBeNil) + + tcpConn, ok := conn.(*net.TCPConn) + So(ok, ShouldBeTrue) + + rawConn, err := tcpConn.SyscallConn() + So(err, ShouldBeNil) + + err = rawConn.Control(func(fd uintptr) { + opt, err = syscall.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT) + }) + So(err, ShouldBeNil) + So(opt, ShouldEqual, 30*time.Second/time.Millisecond) + l.Close() // nolint + }) + }) + Convey("When I create a simple manipulator with default transport, with TCP_USER_TIMEOUT", t, func() { + mm, _ := New( + context.Background(), + "http://url.com/", + OptionTCPUserTimeout(40*time.Second), + ) + + m := mm.(*httpManipulator) + + Convey("Then the dialer is correct", func() { + l, err := net.Listen("tcp", ":0") + So(err, ShouldBeNil) + + opt := -1 + dctx := m.client.Transport.(*http.Transport).DialContext + So(dctx, ShouldNotBeNil) + conn, err := dctx(context.TODO(), "tcp", l.Addr().String()) + So(err, ShouldBeNil) + + tcpConn, ok := conn.(*net.TCPConn) + So(ok, ShouldBeTrue) + + rawConn, err := tcpConn.SyscallConn() + So(err, ShouldBeNil) + + err = rawConn.Control(func(fd uintptr) { + opt, err = syscall.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT) + }) + So(err, ShouldBeNil) + So(opt, ShouldEqual, 40*time.Second/time.Millisecond) + + l.Close() // nolint + }) + }) + Convey("When I create a simple manipulator with default transport, without TCP_USER_TIMEOUT", t, func() { + mm, _ := New( + context.Background(), + "http://url.com/", + ) + + m := mm.(*httpManipulator) + + Convey("Then the dialer is correct", func() { + l, err := net.Listen("tcp4", ":0") + So(err, ShouldBeNil) + + opt := -1 + dctx := m.client.Transport.(*http.Transport).DialContext + So(dctx, ShouldNotBeNil) + conn, err := dctx(context.TODO(), "tcp4", l.Addr().String()) + So(err, ShouldBeNil) + + tcpConn, ok := conn.(*net.TCPConn) + So(ok, ShouldBeTrue) + + rawConn, err := tcpConn.SyscallConn() + So(err, ShouldBeNil) + + err = rawConn.Control(func(fd uintptr) { + opt, err = syscall.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT) + }) + So(err, ShouldBeNil) + So(opt, ShouldEqual, 0) + + l.Close() // nolint + }) + }) +} func TestHTTP_RetrieveMany(t *testing.T) { Convey("Given I have a manipulator and a working server", t, func() { diff --git a/maniphttp/options.go b/maniphttp/options.go index 0c0f7b75..80680046 100644 --- a/maniphttp/options.go +++ b/maniphttp/options.go @@ -14,6 +14,7 @@ package maniphttp import ( "crypto/tls" "net/http" + "time" "go.aporeto.io/elemental" "go.aporeto.io/manipulate" @@ -155,3 +156,11 @@ func OptionSimulateFailures(failureSimulations map[float64]error) Option { m.failureSimulations = failureSimulations } } + +// OptionTCPUserTimeout configures the manipulator to +// have a custom tcp user timeout. +func OptionTCPUserTimeout(t time.Duration) Option { + return func(m *httpManipulator) { + m.tcpUserTimeout = t + } +} diff --git a/maniphttp/options_test.go b/maniphttp/options_test.go index b3ecb7b0..389134f3 100644 --- a/maniphttp/options_test.go +++ b/maniphttp/options_test.go @@ -16,6 +16,7 @@ import ( "crypto/tls" "net/http" "testing" + "time" . "github.com/smartystreets/goconvey/convey" "go.aporeto.io/elemental" @@ -121,4 +122,11 @@ func Test_Options(t *testing.T) { OptionSendCredentialsAsCookie("x-token")(m) So(m.tokenCookieKey, ShouldEqual, "x-token") }) + + Convey("Calling OptionTcpUserTimeout should work", t, func() { + m := &httpManipulator{} + t := 10 * time.Second + OptionTCPUserTimeout(t)(m) + So(m.tcpUserTimeout, ShouldEqual, t) + }) } diff --git a/maniphttp/utils.go b/maniphttp/utils.go index d8689dc2..8e58e6cc 100644 --- a/maniphttp/utils.go +++ b/maniphttp/utils.go @@ -27,6 +27,7 @@ import ( "go.aporeto.io/elemental" "go.aporeto.io/manipulate" "go.aporeto.io/manipulate/maniphttp/internal/compiler" + "go.aporeto.io/manipulate/maniphttp/internal/syscall" ) // AddQueryParameters appends each key-value pair from ctx.Parameters @@ -129,11 +130,12 @@ func getDefaultTLSConfig() *tls.Config { } } -func getDefaultHTTPTransport(url string, disableCompression bool) (*http.Transport, string) { +func getDefaultHTTPTransport(url string, disableCompression bool, tcpUserTimeout time.Duration) (*http.Transport, string) { dialer := (&net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 30 * time.Second, + Control: syscall.MakeDialerControlFunc(tcpUserTimeout), }).DialContext outURL := url