Skip to content

Commit

Permalink
ManipHttp: Add client TCP_USERTIMEOUT option to the socket. (#121)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
abhijitherekar authored Jul 14, 2020
1 parent 14e7e30 commit 767644a
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 9 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
33 changes: 33 additions & 0 deletions maniphttp/internal/syscall/syscall_linux.go
Original file line number Diff line number Diff line change
@@ -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
}
}
15 changes: 15 additions & 0 deletions maniphttp/internal/syscall/syscall_nonlinux.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 9 additions & 8 deletions maniphttp/manipulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
126 changes: 126 additions & 0 deletions maniphttp/manipulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httptest"
"sync/atomic"
"syscall"
"testing"
"time"

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down
9 changes: 9 additions & 0 deletions maniphttp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package maniphttp
import (
"crypto/tls"
"net/http"
"time"

"go.aporeto.io/elemental"
"go.aporeto.io/manipulate"
Expand Down Expand Up @@ -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
}
}
8 changes: 8 additions & 0 deletions maniphttp/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"crypto/tls"
"net/http"
"testing"
"time"

. "github.com/smartystreets/goconvey/convey"
"go.aporeto.io/elemental"
Expand Down Expand Up @@ -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)
})
}
4 changes: 3 additions & 1 deletion maniphttp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 767644a

Please sign in to comment.