From b95cfc4e7445cb804777ee531716bec773a7b170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Tue, 14 Feb 2023 15:10:56 +0300 Subject: [PATCH 01/32] use distinct client for each iteration --- core/scenario/requester/base.go | 1 + core/scenario/requester/http.go | 71 ++++++++++++++++++++-------- core/scenario/requester/http_test.go | 1 + core/scenario/service.go | 9 ++++ core/scenario/service_test.go | 4 ++ 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/core/scenario/requester/base.go b/core/scenario/requester/base.go index d5bbe239..ea576678 100644 --- a/core/scenario/requester/base.go +++ b/core/scenario/requester/base.go @@ -34,6 +34,7 @@ type Requester interface { Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error Send(envs map[string]interface{}) *types.ScenarioStepResult Done() + Type() string } // NewRequester is the factory method of the Requester. diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 1d229818..ae580751 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -73,23 +73,6 @@ func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAdd h.dynamicRgx = regexp.MustCompile(regex.DynamicVariableRegex) h.envRgx = regexp.MustCompile(regex.EnvironmentVariableRegex) - // TlsConfig - tlsConfig := h.initTLSConfig() - - // Transport segment - tr := h.initTransport(tlsConfig) - - // http client - h.client = &http.Client{Transport: tr, Timeout: time.Duration(h.packet.Timeout) * time.Second} - if val, ok := h.packet.Custom["disable-redirect"]; ok { - val := val.(bool) - if val { - h.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - } - } - // Request instance err = h.initRequestInstance() if err != nil { @@ -188,6 +171,28 @@ func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioSt usableVars[k] = v } + if h.client == nil { + h.client = &http.Client{} + } + + // Transport segment + if h.client.Transport == nil { + h.client.Transport = h.initTransport() + } else { + h.updateTransport() + } + + // http client + h.client.Timeout = time.Duration(h.packet.Timeout) * time.Second + if val, ok := h.packet.Custom["disable-redirect"]; ok { + val := val.(bool) + if val { + h.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + } + } + durations := &duration{} trace := newTrace(durations, h.proxyAddr) httpReq, err := h.prepareReq(usableVars, trace) @@ -448,9 +453,9 @@ func fetchErrType(err error) types.RequestError { return requestErr } -func (h *HttpRequester) initTransport(tlsConfig *tls.Config) *http.Transport { +func (h *HttpRequester) initTransport() *http.Transport { tr := &http.Transport{ - TLSClientConfig: tlsConfig, + TLSClientConfig: h.initTLSConfig(), Proxy: http.ProxyURL(h.proxyAddr), MaxIdleConnsPerHost: 60000, MaxIdleConns: 0, @@ -472,6 +477,26 @@ func (h *HttpRequester) initTransport(tlsConfig *tls.Config) *http.Transport { return tr } +func (h *HttpRequester) updateTransport() { + tr := h.client.Transport.(*http.Transport) + tr.TLSClientConfig = h.initTLSConfig() + tr.Proxy = http.ProxyURL(h.proxyAddr) + + tr.DisableKeepAlives = false + if val, ok := h.packet.Custom["keep-alive"]; ok { + tr.DisableKeepAlives = !val.(bool) + } + if val, ok := h.packet.Custom["disable-compression"]; ok { + tr.DisableCompression = val.(bool) + } + if val, ok := h.packet.Custom["h2"]; ok { + val := val.(bool) + if val { + http2.ConfigureTransport(tr) + } + } +} + func (h *HttpRequester) initTLSConfig() *tls.Config { tlsConfig := &tls.Config{ InsecureSkipVerify: true, @@ -526,6 +551,14 @@ func (h *HttpRequester) initRequestInstance() (err error) { return } +func (h *HttpRequester) Type() string { + return "HTTP" +} + +func (h *HttpRequester) SetClient(c *http.Client) { + h.client = c +} + func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { var dnsStart, connStart, tlsStart, reqStart, serverProcessStart time.Time diff --git a/core/scenario/requester/http_test.go b/core/scenario/requester/http_test.go index 9cd7d0a5..7101e5df 100644 --- a/core/scenario/requester/http_test.go +++ b/core/scenario/requester/http_test.go @@ -158,6 +158,7 @@ func TestInitClient(t *testing.T) { tf := func(t *testing.T) { h := &HttpRequester{} h.Init(test.ctx, test.scenarioItem, test.proxy, false, nil) + h.Send(map[string]interface{}{}) transport := h.client.Transport.(*http.Transport) tls := transport.TLSClientConfig diff --git a/core/scenario/service.go b/core/scenario/service.go index e40e53a3..655521ad 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -23,6 +23,7 @@ package scenario import ( "context" "math/rand" + "net/http" "net/url" "regexp" "strconv" @@ -109,6 +110,14 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( s.enrichEnvFromData(envs) atomic.AddInt64(&s.iterIndex, 1) + client := &http.Client{} + for _, sr := range requesters { + if sr.requester.Type() == "HTTP" { + // use same client throughout iteration + sr.requester.(*requester.HttpRequester).SetClient(client) + } + } + for _, sr := range requesters { res := sr.requester.Send(envs) diff --git a/core/scenario/service_test.go b/core/scenario/service_test.go index 4bdae6dc..38fceecf 100644 --- a/core/scenario/service_test.go +++ b/core/scenario/service_test.go @@ -63,6 +63,10 @@ func (m *MockRequester) Done() { m.DoneCalled = true } +func (m *MockRequester) Type() string { + return "mock" +} + type MockSleep struct { SleepCalled bool SleepCallCount int From 907e26d215f0033e780aed8857db0d8aec24bf6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Tue, 14 Feb 2023 17:20:04 +0300 Subject: [PATCH 02/32] refactor requester interface --- core/scenario/requester/base.go | 10 ++++++--- core/scenario/requester/http.go | 30 ++++++++++++--------------- core/scenario/requester/http_test.go | 17 ++++++++------- core/scenario/service.go | 24 ++++++++++++++------- core/scenario/service_test.go | 31 ++++++++++++++-------------- 5 files changed, 61 insertions(+), 51 deletions(-) diff --git a/core/scenario/requester/base.go b/core/scenario/requester/base.go index ea576678..f63377b7 100644 --- a/core/scenario/requester/base.go +++ b/core/scenario/requester/base.go @@ -22,6 +22,7 @@ package requester import ( "context" + "net/http" "net/url" "go.ddosify.com/ddosify/core/scenario/scripting/injection" @@ -31,10 +32,13 @@ import ( // Requester is the interface that abstracts different protocols' request sending implementations. // Protocol field in the types.ScenarioStep determines which requester implementation to use. type Requester interface { - Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error - Send(envs map[string]interface{}) *types.ScenarioStepResult - Done() Type() string + Done() +} + +type HttpRequesterI interface { + Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error + Send(*http.Client, map[string]interface{}) *types.ScenarioStepResult } // NewRequester is the factory method of the Requester. diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index ae580751..f037281e 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -50,7 +50,6 @@ type HttpRequester struct { ctx context.Context proxyAddr *url.URL packet types.ScenarioStep - client *http.Client request *http.Request ei *injection.EnvironmentInjector containsDynamicField map[string]bool @@ -148,10 +147,12 @@ func (h *HttpRequester) Done() { // let us reuse the connections when keep-alive enabled(default) // When the Job is finished, we have to Close idle connections to prevent sockets to lock in at the TIME_WAIT state. // Otherwise, the next job can't use these sockets because they are reserved for the current target host. - h.client.CloseIdleConnections() + + // TODO + // h.client.CloseIdleConnections() } -func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioStepResult) { +func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { var statusCode int var contentLength int64 var requestErr types.RequestError @@ -171,23 +172,23 @@ func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioSt usableVars[k] = v } - if h.client == nil { - h.client = &http.Client{} + if client == nil { + client = &http.Client{} } // Transport segment - if h.client.Transport == nil { - h.client.Transport = h.initTransport() + if client.Transport == nil { + client.Transport = h.initTransport() } else { - h.updateTransport() + h.updateTransport(client.Transport.(*http.Transport)) } // http client - h.client.Timeout = time.Duration(h.packet.Timeout) * time.Second + client.Timeout = time.Duration(h.packet.Timeout) * time.Second if val, ok := h.packet.Custom["disable-redirect"]; ok { val := val.(bool) if val { - h.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } } @@ -214,7 +215,7 @@ func (h *HttpRequester) Send(envs map[string]interface{}) (res *types.ScenarioSt httpReq.Body = io.NopCloser(bytes.NewReader(copiedReqBody.Bytes())) // Action - httpRes, err := h.client.Do(httpReq) + httpRes, err := client.Do(httpReq) if err != nil { requestErr = fetchErrType(err) failedCaptures = h.captureEnvironmentVariables(nil, nil, extractedVars) @@ -477,8 +478,7 @@ func (h *HttpRequester) initTransport() *http.Transport { return tr } -func (h *HttpRequester) updateTransport() { - tr := h.client.Transport.(*http.Transport) +func (h *HttpRequester) updateTransport(tr *http.Transport) { tr.TLSClientConfig = h.initTLSConfig() tr.Proxy = http.ProxyURL(h.proxyAddr) @@ -555,10 +555,6 @@ func (h *HttpRequester) Type() string { return "HTTP" } -func (h *HttpRequester) SetClient(c *http.Client) { - h.client = c -} - func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { var dnsStart, connStart, tlsStart, reqStart, serverProcessStart time.Time diff --git a/core/scenario/requester/http_test.go b/core/scenario/requester/http_test.go index 7101e5df..16e9627a 100644 --- a/core/scenario/requester/http_test.go +++ b/core/scenario/requester/http_test.go @@ -158,9 +158,10 @@ func TestInitClient(t *testing.T) { tf := func(t *testing.T) { h := &HttpRequester{} h.Init(test.ctx, test.scenarioItem, test.proxy, false, nil) - h.Send(map[string]interface{}{}) + client := &http.Client{} + h.Send(client, map[string]interface{}{}) - transport := h.client.Transport.(*http.Transport) + transport := client.Transport.(*http.Transport) tls := transport.TLSClientConfig // TLS Assert (Also check HTTP2 vs HTTP) @@ -188,11 +189,11 @@ func TestInitClient(t *testing.T) { } // Client Assert - if test.client.Timeout != h.client.Timeout { - t.Errorf("Timeout Expected %v, Found %v", test.client.Timeout, h.client.Timeout) + if test.client.Timeout != client.Timeout { + t.Errorf("Timeout Expected %v, Found %v", test.client.Timeout, client.Timeout) } - crFunc := h.client.CheckRedirect == nil + crFunc := client.CheckRedirect == nil expectedCRFunc := test.client.CheckRedirect == nil if expectedCRFunc != crFunc { t.Errorf("CheckRedirect Expected %v, Found %v", expectedCRFunc, crFunc) @@ -368,7 +369,7 @@ func TestSendOnDebugModePopulatesDebugInfo(t *testing.T) { var proxy *url.URL _ = h.Init(ctx, s, proxy, debug, nil) envs := map[string]interface{}{} - res := h.Send(envs) + res := h.Send(http.DefaultClient, envs) if expectedMethod != res.Method { t.Errorf("Method Expected %#v, Found: \n%#v", expectedMethod, res.Method) @@ -428,7 +429,7 @@ func TestCaptureEnvShouldSetEmptyStringWhenReqFails(t *testing.T) { var proxy *url.URL _ = h.Init(ctx, test.scenarioStep, proxy, debug, nil) envs := map[string]interface{}{} - res := h.Send(envs) + res := h.Send(http.DefaultClient, envs) if !reflect.DeepEqual(res.ExtractedEnvs, test.expectedExtractedEnvs) { t.Errorf("Extracted env should be set empty string on req failure") @@ -467,7 +468,7 @@ func TestAssertions(t *testing.T) { h := &HttpRequester{} h.Init(ctx, s, nil, false, nil) - res := h.Send(map[string]interface{}{}) + res := h.Send(http.DefaultClient, map[string]interface{}{}) if !strings.EqualFold(res.FailedAssertions[0].Rule, rule1) { t.Errorf("rule expected %s, got %s", rule1, res.FailedAssertions[0].Rule) diff --git a/core/scenario/service.go b/core/scenario/service.go index 655521ad..688fae1b 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -22,6 +22,7 @@ package scenario import ( "context" + "fmt" "math/rand" "net/http" "net/url" @@ -112,14 +113,14 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( client := &http.Client{} for _, sr := range requesters { - if sr.requester.Type() == "HTTP" { - // use same client throughout iteration - sr.requester.(*requester.HttpRequester).SetClient(client) + var res *types.ScenarioStepResult + switch sr.requester.Type() { + case "HTTP": + httpRequester := sr.requester.(requester.HttpRequesterI) + res = httpRequester.Send(client, envs) + default: + res = &types.ScenarioStepResult{Err: types.RequestError{Type: fmt.Sprintf("type not defined: %s", sr.requester.Type())}} } - } - - for _, sr := range requesters { - res := sr.requester.Send(envs) if res.Err.Type == types.ErrorProxy || res.Err.Type == types.ErrorIntented { err = &res.Err @@ -208,7 +209,14 @@ func (s *ScenarioService) createRequesters(proxy *url.URL) (err error) { }, ) - err = r.Init(s.ctx, si, proxy, s.debug, s.ei) + switch r.Type() { + case "HTTP": + httpRequester := r.(requester.HttpRequesterI) + err = httpRequester.Init(s.ctx, si, proxy, s.debug, s.ei) + default: + err = fmt.Errorf("type not defined: %s", r.Type()) + } + if err != nil { return } diff --git a/core/scenario/service_test.go b/core/scenario/service_test.go index 38fceecf..7c9e5960 100644 --- a/core/scenario/service_test.go +++ b/core/scenario/service_test.go @@ -23,6 +23,7 @@ package scenario import ( "context" "fmt" + "net/http" "net/url" "reflect" "testing" @@ -33,7 +34,7 @@ import ( "go.ddosify.com/ddosify/core/types" ) -type MockRequester struct { +type MockHttpRequester struct { InitCalled bool SendCalled bool DoneCalled bool @@ -46,7 +47,7 @@ type MockRequester struct { ReturnSend *types.ScenarioStepResult } -func (m *MockRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { +func (m *MockHttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { m.InitCalled = true if m.FailInit { return fmt.Errorf(m.FailInitMsg) @@ -54,17 +55,17 @@ func (m *MockRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAdd return } -func (m *MockRequester) Send(envs map[string]interface{}) (res *types.ScenarioStepResult) { +func (m *MockHttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { m.SendCalled = true return m.ReturnSend } -func (m *MockRequester) Done() { +func (m *MockHttpRequester) Done() { m.DoneCalled = true } -func (m *MockRequester) Type() string { - return "mock" +func (m *MockHttpRequester) Type() string { + return "HTTP" } type MockSleep struct { @@ -230,11 +231,11 @@ func TestDo(t *testing.T) { { scenarioItemID: 1, sleeper: mockSleep, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}}, }, { scenarioItemID: 2, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}}, }, } service := ScenarioService{ @@ -292,19 +293,19 @@ func TestDoErrorOnSend(t *testing.T) { requestersProxyError := []scenarioItemRequester{ { scenarioItemID: 1, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorProxy}}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorProxy}}}, }, } requestersIntentedError := []scenarioItemRequester{ { scenarioItemID: 1, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorIntented}}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorIntented}}}, }, } requestersConnError := []scenarioItemRequester{ { scenarioItemID: 1, - requester: &MockRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorConn}}}, + requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorConn}}}, }, } @@ -409,10 +410,10 @@ func TestDone(t *testing.T) { p2, _ := url.Parse("http://proxy_server.com:8080") ctx := context.TODO() - requester1 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} - requester2 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} - requester3 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} - requester4 := &MockRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} + requester1 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} + requester2 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} + requester3 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} + requester4 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: { From b75fc2649ac219dcd4d37892907ef8ea5ae637fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 12:38:27 +0300 Subject: [PATCH 03/32] add client pool and use same connection per host throughout an iteration --- core/engine.go | 18 +++++- core/scenario/client_pool.go | 101 ++++++++++++++++++++++++++++++++ core/scenario/requester/http.go | 7 +-- core/scenario/service.go | 21 ++++++- core/scenario/service_test.go | 26 ++++++-- 5 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 core/scenario/client_pool.go diff --git a/core/engine.go b/core/engine.go index 82f6b2e6..a8668cbe 100644 --- a/core/engine.go +++ b/core/engine.go @@ -91,11 +91,16 @@ func NewEngine(ctx context.Context, h types.Hammer) (e *engine, err error) { } func (e *engine) Init() (err error) { + e.initReqCountArr() if err = e.proxyService.Init(e.hammer.Proxy); err != nil { return } - if err = e.scenarioService.Init(e.ctx, e.hammer.Scenario, e.proxyService.GetAll(), e.hammer.Debug); err != nil { + if err = e.scenarioService.Init(e.ctx, e.hammer.Scenario, e.proxyService.GetAll(), scenario.ScenarioOpts{ + Debug: e.hammer.Debug, + IterationCount: e.hammer.IterationCount, + MaxConcurrentIterCount: e.getMaxConcurrentIterCount(), + }); err != nil { return } @@ -103,7 +108,6 @@ func (e *engine) Init() (err error) { return } - e.initReqCountArr() return } @@ -184,6 +188,16 @@ func (e *engine) stop() { e.scenarioService.Done() } +func (e *engine) getMaxConcurrentIterCount() int { + max := 0 + for _, v := range e.reqCountArr { + if v > max { + max = v + } + } + return max +} + func (e *engine) initReqCountArr() { if e.hammer.Debug { e.reqCountArr = []int{1} diff --git a/core/scenario/client_pool.go b/core/scenario/client_pool.go new file mode 100644 index 00000000..fa0ae491 --- /dev/null +++ b/core/scenario/client_pool.go @@ -0,0 +1,101 @@ +package scenario + +import ( + "errors" + "net/http" + "sync" +) + +type clientPool struct { + // storage for our http.Clients + mu sync.RWMutex + clients chan *http.Client + factory Factory +} + +// Factory is a function to create new connections. +type Factory func() *http.Client + +// NewClientPool returns a new pool based on buffered channels with an initial +// capacity and maximum capacity. Factory is used when initial capacity is +// greater than zero to fill the pool. A zero initialCap doesn't fill the Pool +// until a new Get() is called. During a Get(), If there is no new client +// available in the pool, a new client will be created via the Factory() +// method. +func NewClientPool(initialCap, maxCap int, factory Factory) (*clientPool, error) { + if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { + return nil, errors.New("invalid capacity settings") + } + + pool := &clientPool{ + clients: make(chan *http.Client, maxCap), + factory: factory, + } + + // create initial clients, if something goes wrong, + // just close the pool error out. + for i := 0; i < initialCap; i++ { + client := pool.factory() + pool.clients <- client + } + + return pool, nil +} + +func (c *clientPool) getConnsAndFactory() (chan *http.Client, Factory) { + c.mu.RLock() + clients := c.clients + factory := c.factory + c.mu.RUnlock() + return clients, factory +} + +func (c *clientPool) Get() *http.Client { + clients, factory := c.getConnsAndFactory() + + var client *http.Client + select { + case client = <-clients: + default: + client = factory() + } + return client +} + +func (c *clientPool) Put(client *http.Client) error { + if client == nil { + return errors.New("client is nil. rejecting") + } + + c.mu.RLock() + defer c.mu.RUnlock() + + if c.clients == nil { + // pool is closed, close passed client + client.CloseIdleConnections() + return nil + } + + // put the resource back into the pool. If the pool is full, this will + // block and the default case will be executed. + select { + case c.clients <- client: + return nil + default: + // pool is full, close passed connection + client.CloseIdleConnections() + return nil + } +} + +func (c *clientPool) Len() int { + conns, _ := c.getConnsAndFactory() + return len(conns) +} + +func (c *clientPool) Done() { + close(c.clients) + for c := range c.clients { + c.CloseIdleConnections() + } +} diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index f037281e..861f65de 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -456,10 +456,9 @@ func fetchErrType(err error) types.RequestError { func (h *HttpRequester) initTransport() *http.Transport { tr := &http.Transport{ - TLSClientConfig: h.initTLSConfig(), - Proxy: http.ProxyURL(h.proxyAddr), - MaxIdleConnsPerHost: 60000, - MaxIdleConns: 0, + TLSClientConfig: h.initTLSConfig(), + Proxy: http.ProxyURL(h.proxyAddr), + MaxConnsPerHost: 1, // to use the same connection per host throughout an iteration } tr.DisableKeepAlives = false diff --git a/core/scenario/service.go b/core/scenario/service.go index 688fae1b..d7c93d50 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -46,6 +46,8 @@ type ScenarioService struct { // Each scenarioItem has a requester clients map[*url.URL][]scenarioItemRequester + cPool *clientPool + scenario types.Scenario ctx context.Context @@ -60,13 +62,19 @@ func NewScenarioService() *ScenarioService { return &ScenarioService{} } +type ScenarioOpts struct { + Debug bool + IterationCount int + MaxConcurrentIterCount int +} + // Init initializes the ScenarioService.clients with the given types.Scenario and proxies. // Passes the given ctx to the underlying requestor so we are able to control the life of each request. func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, - proxies []*url.URL, debug bool) (err error) { + proxies []*url.URL, opts ScenarioOpts) (err error) { s.scenario = scenario s.ctx = ctx - s.debug = debug + s.debug = opts.Debug s.clients = make(map[*url.URL][]scenarioItemRequester, len(proxies)) ei := &injection.EnvironmentInjector{} @@ -82,6 +90,10 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi := &injection.EnvironmentInjector{} vi.Init() s.ei = vi + + // TODO: timeout and buffer + s.cPool, err = NewClientPool(opts.MaxConcurrentIterCount, opts.IterationCount, func() *http.Client { return &http.Client{} }) + return } @@ -111,7 +123,7 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( s.enrichEnvFromData(envs) atomic.AddInt64(&s.iterIndex, 1) - client := &http.Client{} + client := s.cPool.Get() for _, sr := range requesters { var res *types.ScenarioStepResult switch sr.requester.Type() { @@ -138,6 +150,7 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( enrichEnvFromPrevStep(envs, res.ExtractedEnvs) } + s.cPool.Put(client) return } @@ -176,6 +189,8 @@ func (s *ScenarioService) Done() { r.requester.Done() } } + + s.cPool.Done() } func (s *ScenarioService) getOrCreateRequesters(proxy *url.URL) (requesters []scenarioItemRequester, err error) { diff --git a/core/scenario/service_test.go b/core/scenario/service_test.go index 7c9e5960..5ffe7231 100644 --- a/core/scenario/service_test.go +++ b/core/scenario/service_test.go @@ -191,7 +191,11 @@ func TestInitService(t *testing.T) { // Act service := ScenarioService{} - err := service.Init(ctx, scenario, proxies, false) + err := service.Init(ctx, scenario, proxies, ScenarioOpts{ + Debug: false, + IterationCount: 1, + MaxConcurrentIterCount: 1, + }) // Assert if err != nil { @@ -238,12 +242,14 @@ func TestDo(t *testing.T) { requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}}, }, } + cPool, _ := NewClientPool(1, 1, func() *http.Client { return &http.Client{} }) service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: requesters, }, scenario: scenario, ctx: ctx, + cPool: cPool, } expectedResponse := types.ScenarioResult{ @@ -322,12 +328,14 @@ func TestDoErrorOnSend(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { + cPool, _ := NewClientPool(1, 1, func() *http.Client { return &http.Client{} }) service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: test.requesters, }, scenario: scenario, ctx: ctx, + cPool: cPool, } // Act @@ -410,6 +418,8 @@ func TestDone(t *testing.T) { p2, _ := url.Parse("http://proxy_server.com:8080") ctx := context.TODO() + cPool, _ := NewClientPool(1, 1, func() *http.Client { return &http.Client{} }) + requester1 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} requester2 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} requester3 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} @@ -439,8 +449,8 @@ func TestDone(t *testing.T) { }, scenario: scenario, ctx: ctx, + cPool: cPool, } - // Act service.Done() @@ -478,7 +488,11 @@ func TestGetOrCreateRequesters(t *testing.T) { ctx := context.TODO() service := ScenarioService{} - service.Init(ctx, scenario, proxies, false) + service.Init(ctx, scenario, proxies, ScenarioOpts{ + Debug: false, + IterationCount: 1, + MaxConcurrentIterCount: 1, + }) expectedRequesters := []scenarioItemRequester{{scenarioItemID: 1, requester: &requester.HttpRequester{}}} expectedClients := map[*url.URL][]scenarioItemRequester{ @@ -523,7 +537,11 @@ func TestGetOrCreateRequestersNewProxy(t *testing.T) { ctx := context.TODO() service := ScenarioService{} - service.Init(ctx, scenario, proxies, false) + service.Init(ctx, scenario, proxies, ScenarioOpts{ + Debug: false, + IterationCount: 1, + MaxConcurrentIterCount: 1, + }) expectedRequesters := []scenarioItemRequester{{scenarioItemID: 1, requester: &requester.HttpRequester{}}} From a358b4348c577f9c971cf00b1f9799dc74a82d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 14:44:52 +0300 Subject: [PATCH 04/32] update benchmark test exit code --- main_benchmark_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/main_benchmark_test.go b/main_benchmark_test.go index ecf0bfb0..ff340795 100644 --- a/main_benchmark_test.go +++ b/main_benchmark_test.go @@ -214,10 +214,9 @@ func BenchmarkEngines(t *testing.B) { }) - if success { - os.Exit(0) + if !success { + os.Exit(1) } - os.Exit(1) } } From 0b5f4fcf2bcbd834df00590d87b46ac24b3b827b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 14:49:49 +0300 Subject: [PATCH 05/32] update jenkins benchmark --- Jenkinsfile_benchmark | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index 8e24202a..22fb29af 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -14,7 +14,7 @@ pipeline { stage('Performance Test') { steps { lock('multi_branch_server_benchmark') { - sh 'GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' + sh 'GOCACHE=/tmp/ set -o pipefail && go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' } } } @@ -29,7 +29,7 @@ pipeline { } steps { lock('multi_branch_server_benchmark') { - sh 'git fetch origin develop:develop || git checkout develop && git pull && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' + sh 'git fetch origin develop:develop || git checkout develop && git pull && GOCACHE=/tmp/ set -o pipefail && go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' sh "git checkout ${BRANCH_NAME}" sh "benchstat -alpha 1.01 --sort delta gobench_develop.txt gobench_branch.txt | tee gobench_branch_result.txt" sh "benchstat -alpha 1.01 --sort delta --html gobench_develop.txt gobench_branch.txt > 00_gobench_result.html && echo ${BUILD_URL}artifact/00_gobench_result.html" From 4ed6d405cf94a6da68161afb2749393047937f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 14:51:33 +0300 Subject: [PATCH 06/32] update jenkins --- Jenkinsfile_benchmark | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index 22fb29af..21b4f74a 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -14,7 +14,7 @@ pipeline { stage('Performance Test') { steps { lock('multi_branch_server_benchmark') { - sh 'GOCACHE=/tmp/ set -o pipefail && go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' + sh 'set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' } } } @@ -29,7 +29,7 @@ pipeline { } steps { lock('multi_branch_server_benchmark') { - sh 'git fetch origin develop:develop || git checkout develop && git pull && GOCACHE=/tmp/ set -o pipefail && go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' + sh 'git fetch origin develop:develop || git checkout develop && git pull && set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' sh "git checkout ${BRANCH_NAME}" sh "benchstat -alpha 1.01 --sort delta gobench_develop.txt gobench_branch.txt | tee gobench_branch_result.txt" sh "benchstat -alpha 1.01 --sort delta --html gobench_develop.txt gobench_branch.txt > 00_gobench_result.html && echo ${BUILD_URL}artifact/00_gobench_result.html" From fc1b4b76d392bac2ce966925a767134c32115f33 Mon Sep 17 00:00:00 2001 From: fatihbaltaci Date: Thu, 16 Feb 2023 15:00:49 +0300 Subject: [PATCH 07/32] Update jenkinsfile --- Jenkinsfile_benchmark | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index 21b4f74a..f45f930f 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -14,7 +14,8 @@ pipeline { stage('Performance Test') { steps { lock('multi_branch_server_benchmark') { - sh 'set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' + sh 'set -o pipefail' + sh 'GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' } } } @@ -29,7 +30,7 @@ pipeline { } steps { lock('multi_branch_server_benchmark') { - sh 'git fetch origin develop:develop || git checkout develop && git pull && set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' + sh 'git fetch origin develop:develop || git checkout develop && git pull && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' sh "git checkout ${BRANCH_NAME}" sh "benchstat -alpha 1.01 --sort delta gobench_develop.txt gobench_branch.txt | tee gobench_branch_result.txt" sh "benchstat -alpha 1.01 --sort delta --html gobench_develop.txt gobench_branch.txt > 00_gobench_result.html && echo ${BUILD_URL}artifact/00_gobench_result.html" From 56eff2a3795078cf03df69de4f849250bd40b85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 15:03:21 +0300 Subject: [PATCH 08/32] add connection reuse to conf --- config/json.go | 33 ++++++++++++++++++--------------- core/engine.go | 1 + core/scenario/service.go | 11 +++++++++-- core/types/hammer.go | 3 +++ 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/config/json.go b/config/json.go index 0d80fd61..df62a70b 100644 --- a/config/json.go +++ b/config/json.go @@ -144,26 +144,28 @@ func (c *CsvConf) UnmarshalJSON(data []byte) error { } type JsonReader struct { - ReqCount *int `json:"request_count"` - IterCount *int `json:"iteration_count"` - LoadType string `json:"load_type"` - Duration int `json:"duration"` - TimeRunCount timeRunCount `json:"manual_load"` - Steps []step `json:"steps"` - Output string `json:"output"` - Proxy string `json:"proxy"` - Envs map[string]interface{} `json:"env"` - Data map[string]CsvConf `json:"data"` - Debug bool `json:"debug"` - SamplingRate *int `json:"sampling_rate"` + ReqCount *int `json:"request_count"` + IterCount *int `json:"iteration_count"` + LoadType string `json:"load_type"` + Duration int `json:"duration"` + TimeRunCount timeRunCount `json:"manual_load"` + Steps []step `json:"steps"` + Output string `json:"output"` + Proxy string `json:"proxy"` + Envs map[string]interface{} `json:"env"` + Data map[string]CsvConf `json:"data"` + Debug bool `json:"debug"` + SamplingRate *int `json:"sampling_rate"` + ConnectionReuse bool `json:"connection_reuse"` } func (j *JsonReader) UnmarshalJSON(data []byte) error { type jsonReaderAlias JsonReader defaultFields := &jsonReaderAlias{ - LoadType: types.DefaultLoadType, - Duration: types.DefaultDuration, - Output: types.DefaultOutputType, + LoadType: types.DefaultLoadType, + Duration: types.DefaultDuration, + Output: types.DefaultOutputType, + ConnectionReuse: true, } err := json.Unmarshal(data, defaultFields) @@ -275,6 +277,7 @@ func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { ReportDestination: j.Output, Debug: j.Debug, SamplingRate: samplingRate, + ConnectionReuse: j.ConnectionReuse, } return } diff --git a/core/engine.go b/core/engine.go index a8668cbe..35f00f72 100644 --- a/core/engine.go +++ b/core/engine.go @@ -100,6 +100,7 @@ func (e *engine) Init() (err error) { Debug: e.hammer.Debug, IterationCount: e.hammer.IterationCount, MaxConcurrentIterCount: e.getMaxConcurrentIterCount(), + ConnectionReuse: e.hammer.ConnectionReuse, }); err != nil { return } diff --git a/core/scenario/service.go b/core/scenario/service.go index d7c93d50..f2059c82 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -66,6 +66,7 @@ type ScenarioOpts struct { Debug bool IterationCount int MaxConcurrentIterCount int + ConnectionReuse bool } // Init initializes the ScenarioService.clients with the given types.Scenario and proxies. @@ -91,8 +92,14 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi.Init() s.ei = vi - // TODO: timeout and buffer - s.cPool, err = NewClientPool(opts.MaxConcurrentIterCount, opts.IterationCount, func() *http.Client { return &http.Client{} }) + initialClientCount := opts.MaxConcurrentIterCount + if !opts.ConnectionReuse { + // TODO: timeout and buffer + initialClientCount = opts.IterationCount + } + maxClientCount := opts.IterationCount + + s.cPool, err = NewClientPool(initialClientCount, maxClientCount, func() *http.Client { return &http.Client{} }) return } diff --git a/core/types/hammer.go b/core/types/hammer.go index d0c9537e..86417665 100644 --- a/core/types/hammer.go +++ b/core/types/hammer.go @@ -85,6 +85,9 @@ type Hammer struct { // Sampling rate SamplingRate int + + // Connection reuse + ConnectionReuse bool } // Validate validates attack metadata and executes the validation methods of the services. From 6a7d09a473bf3316b7a9a65d7bd40624e76fd5fd Mon Sep 17 00:00:00 2001 From: fatihbaltaci Date: Thu, 16 Feb 2023 15:10:30 +0300 Subject: [PATCH 09/32] Update jenkinsfile --- Jenkinsfile_benchmark | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index f45f930f..21b4f74a 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -14,8 +14,7 @@ pipeline { stage('Performance Test') { steps { lock('multi_branch_server_benchmark') { - sh 'set -o pipefail' - sh 'GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' + sh 'set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_branch.txt' } } } @@ -30,7 +29,7 @@ pipeline { } steps { lock('multi_branch_server_benchmark') { - sh 'git fetch origin develop:develop || git checkout develop && git pull && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' + sh 'git fetch origin develop:develop || git checkout develop && git pull && set -o pipefail && GOCACHE=/tmp/ go test -benchmem -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 | tee gobench_develop.txt' sh "git checkout ${BRANCH_NAME}" sh "benchstat -alpha 1.01 --sort delta gobench_develop.txt gobench_branch.txt | tee gobench_branch_result.txt" sh "benchstat -alpha 1.01 --sort delta --html gobench_develop.txt gobench_branch.txt > 00_gobench_result.html && echo ${BUILD_URL}artifact/00_gobench_result.html" From 349d3f807d11f6c555f2066c3d5cc38043db9394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 18:21:28 +0300 Subject: [PATCH 10/32] change pool init count --- core/scenario/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scenario/service.go b/core/scenario/service.go index f2059c82..a734cb25 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -92,7 +92,7 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi.Init() s.ei = vi - initialClientCount := opts.MaxConcurrentIterCount + initialClientCount := opts.MaxConcurrentIterCount * 3 if !opts.ConnectionReuse { // TODO: timeout and buffer initialClientCount = opts.IterationCount From ddfd91e3eae8445f16091b9421636bf9d5f4ef10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 18:27:23 +0300 Subject: [PATCH 11/32] fix hammer tests --- config/json_test.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/config/json_test.go b/config/json_test.go index 5408cbc1..687aed88 100644 --- a/config/json_test.go +++ b/config/json_test.go @@ -55,7 +55,8 @@ func TestCreateHammerDefaultValues(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -110,7 +111,8 @@ func TestCreateHammer(t *testing.T) { Strategy: "single", Addr: addr, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -165,7 +167,8 @@ func TestCreateHammerWithIterationCountInsteadOfReqCount(t *testing.T) { Strategy: "single", Addr: addr, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -223,7 +226,8 @@ func TestCreateHammerWithIterationCountOverridesReqCount(t *testing.T) { Strategy: "single", Addr: addr, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -258,7 +262,8 @@ func TestCreateHammerManualLoad(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -293,7 +298,8 @@ func TestCreateHammerManualLoadOverrideOthers(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -551,7 +557,8 @@ func TestCreateHammerTLSWithOnlyCertPath(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -596,7 +603,8 @@ func TestCreateHammerTLSWithOnlyKeyPath(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() @@ -632,7 +640,8 @@ func TestCreateHammerTLSWithWithEmptyPath(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, + SamplingRate: types.DefaultSamplingCount, + ConnectionReuse: true, } h, err := jsonReader.CreateHammer() From 43c4ed25640c981a51d223ffe1219dc33217151d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 18:39:41 +0300 Subject: [PATCH 12/32] change pool count --- core/scenario/client_pool.go | 82 ++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/core/scenario/client_pool.go b/core/scenario/client_pool.go index fa0ae491..4ad28add 100644 --- a/core/scenario/client_pool.go +++ b/core/scenario/client_pool.go @@ -2,15 +2,15 @@ package scenario import ( "errors" + "fmt" "net/http" - "sync" ) type clientPool struct { // storage for our http.Clients - mu sync.RWMutex - clients chan *http.Client + clients []chan *http.Client factory Factory + N int } // Factory is a function to create new connections. @@ -27,37 +27,45 @@ func NewClientPool(initialCap, maxCap int, factory Factory) (*clientPool, error) return nil, errors.New("invalid capacity settings") } + N := 4 + pool := &clientPool{ - clients: make(chan *http.Client, maxCap), + clients: make([]chan *http.Client, N), factory: factory, + N: N, + } + + for i := 0; i < N; i++ { + pool.clients[i] = make(chan *http.Client, maxCap/N) } // create initial clients, if something goes wrong, // just close the pool error out. for i := 0; i < initialCap; i++ { client := pool.factory() - pool.clients <- client + pool.clients[i%N] <- client } return pool, nil } -func (c *clientPool) getConnsAndFactory() (chan *http.Client, Factory) { - c.mu.RLock() - clients := c.clients - factory := c.factory - c.mu.RUnlock() - return clients, factory -} - func (c *clientPool) Get() *http.Client { - clients, factory := c.getConnsAndFactory() - var client *http.Client + // TODO N = 4 select { - case client = <-clients: + case client = <-c.clients[0]: + case client = <-c.clients[1]: + case client = <-c.clients[2]: + case client = <-c.clients[3]: + // case client = <-c.clients[4]: + // case client = <-c.clients[5]: + // case client = <-c.clients[6]: + // case client = <-c.clients[7]: + // case client = <-c.clients[8]: + // case client = <-c.clients[9]: + default: - client = factory() + client = c.factory() } return client } @@ -67,9 +75,6 @@ func (c *clientPool) Put(client *http.Client) error { return errors.New("client is nil. rejecting") } - c.mu.RLock() - defer c.mu.RUnlock() - if c.clients == nil { // pool is closed, close passed client client.CloseIdleConnections() @@ -79,23 +84,46 @@ func (c *clientPool) Put(client *http.Client) error { // put the resource back into the pool. If the pool is full, this will // block and the default case will be executed. select { - case c.clients <- client: + case c.clients[0] <- client: + return nil + case c.clients[1] <- client: + return nil + case c.clients[2] <- client: return nil + case c.clients[3] <- client: + return nil + // case c.clients[4] <- client: + // return nil + // case c.clients[5] <- client: + // return nil + // case c.clients[6] <- client: + // return nil + // case c.clients[7] <- client: + // return nil + // case c.clients[8] <- client: + // return nil + // case c.clients[9] <- client: + // return nil + default: - // pool is full, close passed connection + // pool is full, close passed client client.CloseIdleConnections() return nil } } func (c *clientPool) Len() int { - conns, _ := c.getConnsAndFactory() - return len(conns) + return len(c.clients) } func (c *clientPool) Done() { - close(c.clients) - for c := range c.clients { - c.CloseIdleConnections() + fmt.Println(c.Len()) + for i := 0; i < c.N; i++ { + close(c.clients[i]) + } + for _, cp := range c.clients { + for c := range cp { + c.CloseIdleConnections() + } } } From 990638777ebea4110252f1a495d10888ad8cf488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 18:45:53 +0300 Subject: [PATCH 13/32] change init client count --- core/scenario/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scenario/service.go b/core/scenario/service.go index a734cb25..af03627a 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -92,7 +92,7 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi.Init() s.ei = vi - initialClientCount := opts.MaxConcurrentIterCount * 3 + initialClientCount := opts.MaxConcurrentIterCount * 10 if !opts.ConnectionReuse { // TODO: timeout and buffer initialClientCount = opts.IterationCount From 311dcc56b099b77c1cacc8a24a98132ca7ec64c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 16 Feb 2023 19:03:17 +0300 Subject: [PATCH 14/32] change max client count --- core/scenario/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scenario/service.go b/core/scenario/service.go index af03627a..db123b15 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -97,7 +97,7 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, // TODO: timeout and buffer initialClientCount = opts.IterationCount } - maxClientCount := opts.IterationCount + maxClientCount := opts.IterationCount / 2 s.cPool, err = NewClientPool(initialClientCount, maxClientCount, func() *http.Client { return &http.Client{} }) From 30abd1b8513b0e633c8f84903242be6ef6563acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 12:25:03 +0300 Subject: [PATCH 15/32] try one client, reuse port --- core/scenario/requester/base.go | 2 +- core/scenario/requester/http.go | 35 +++++++++++++++++++++++++-------- core/scenario/service.go | 28 +++++++++++++++----------- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/core/scenario/requester/base.go b/core/scenario/requester/base.go index f63377b7..db8a7d40 100644 --- a/core/scenario/requester/base.go +++ b/core/scenario/requester/base.go @@ -38,7 +38,7 @@ type Requester interface { type HttpRequesterI interface { Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error - Send(*http.Client, map[string]interface{}) *types.ScenarioStepResult + Send(client *http.Client, envs map[string]interface{}, hostPortMap map[string]string) *types.ScenarioStepResult } // NewRequester is the factory method of the Requester. diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 861f65de..f6832729 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -27,6 +27,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/http/httptrace" "net/url" @@ -37,6 +38,7 @@ import ( "time" "github.com/google/uuid" + reuse "github.com/libp2p/go-reuseport" "go.ddosify.com/ddosify/core/scenario/scripting/assertion" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/evaluator" "go.ddosify.com/ddosify/core/scenario/scripting/extraction" @@ -152,7 +154,7 @@ func (h *HttpRequester) Done() { // h.client.CloseIdleConnections() } -func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { +func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}, hostPortMap map[string]string) (res *types.ScenarioStepResult) { var statusCode int var contentLength int64 var requestErr types.RequestError @@ -178,9 +180,9 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) ( // Transport segment if client.Transport == nil { - client.Transport = h.initTransport() + client.Transport = h.initTransport(hostPortMap) } else { - h.updateTransport(client.Transport.(*http.Transport)) + h.updateTransport(client.Transport.(*http.Transport), hostPortMap) } // http client @@ -195,7 +197,7 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) ( } durations := &duration{} - trace := newTrace(durations, h.proxyAddr) + trace := newTrace(durations, h.proxyAddr, h.packet.URL, hostPortMap) httpReq, err := h.prepareReq(usableVars, trace) if err != nil { // could not prepare req @@ -454,11 +456,18 @@ func fetchErrType(err error) types.RequestError { return requestErr } -func (h *HttpRequester) initTransport() *http.Transport { +func (h *HttpRequester) initTransport(hostPortMap map[string]string) *http.Transport { tr := &http.Transport{ TLSClientConfig: h.initTLSConfig(), Proxy: http.ProxyURL(h.proxyAddr), - MaxConnsPerHost: 1, // to use the same connection per host throughout an iteration + // MaxConnsPerHost: 1, // to use the same connection per host throughout an iteration + Dial: func(network, addr string) (net.Conn, error) { + if localAddr, ok := hostPortMap[h.packet.URL]; ok { + return reuse.Dial(network, localAddr, addr) + } else { + return net.Dial(network, addr) + } + }, } tr.DisableKeepAlives = false @@ -477,10 +486,19 @@ func (h *HttpRequester) initTransport() *http.Transport { return tr } -func (h *HttpRequester) updateTransport(tr *http.Transport) { +func (h *HttpRequester) updateTransport(tr *http.Transport, hostPortMap map[string]string) { tr.TLSClientConfig = h.initTLSConfig() tr.Proxy = http.ProxyURL(h.proxyAddr) + // TODO check + tr.Dial = func(network, addr string) (net.Conn, error) { + if localAddr, ok := hostPortMap[h.packet.URL]; ok { + return reuse.Dial(network, localAddr, addr) + } else { + return net.Dial(network, addr) + } + } + tr.DisableKeepAlives = false if val, ok := h.packet.Custom["keep-alive"]; ok { tr.DisableKeepAlives = !val.(bool) @@ -554,7 +572,7 @@ func (h *HttpRequester) Type() string { return "HTTP" } -func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { +func newTrace(duration *duration, proxyAddr *url.URL, reqUrl string, hostPortMap map[string]string) *httptrace.ClientTrace { var dnsStart, connStart, tlsStart, reqStart, serverProcessStart time.Time // According to the doc in the trace.go; @@ -625,6 +643,7 @@ func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { reqStart = time.Now() } m.Unlock() + hostPortMap[reqUrl] = connInfo.Conn.LocalAddr().String() }, WroteRequest: func(w httptrace.WroteRequestInfo) { m.Lock() diff --git a/core/scenario/service.go b/core/scenario/service.go index db123b15..237c2e70 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -46,7 +46,8 @@ type ScenarioService struct { // Each scenarioItem has a requester clients map[*url.URL][]scenarioItemRequester - cPool *clientPool + cPool *clientPool + client *http.Client scenario types.Scenario ctx context.Context @@ -92,14 +93,15 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi.Init() s.ei = vi - initialClientCount := opts.MaxConcurrentIterCount * 10 - if !opts.ConnectionReuse { - // TODO: timeout and buffer - initialClientCount = opts.IterationCount - } - maxClientCount := opts.IterationCount / 2 + // initialClientCount := opts.MaxConcurrentIterCount + // if !opts.ConnectionReuse { + // // TODO: timeout and buffer + // initialClientCount = opts.IterationCount + // } + // maxClientCount := opts.IterationCount + // s.cPool, err = NewClientPool(initialClientCount, maxClientCount, func() *http.Client { return &http.Client{} }) - s.cPool, err = NewClientPool(initialClientCount, maxClientCount, func() *http.Client { return &http.Client{} }) + s.client = &http.Client{} return } @@ -130,13 +132,15 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( s.enrichEnvFromData(envs) atomic.AddInt64(&s.iterIndex, 1) - client := s.cPool.Get() + // client := s.cPool.Get() + + urlPortMap := make(map[string]string) for _, sr := range requesters { var res *types.ScenarioStepResult switch sr.requester.Type() { case "HTTP": httpRequester := sr.requester.(requester.HttpRequesterI) - res = httpRequester.Send(client, envs) + res = httpRequester.Send(s.client, envs, urlPortMap) // TODO default: res = &types.ScenarioStepResult{Err: types.RequestError{Type: fmt.Sprintf("type not defined: %s", sr.requester.Type())}} } @@ -157,7 +161,7 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( enrichEnvFromPrevStep(envs, res.ExtractedEnvs) } - s.cPool.Put(client) + // s.cPool.Put(client) return } @@ -197,7 +201,7 @@ func (s *ScenarioService) Done() { } } - s.cPool.Done() + // s.cPool.Done() } func (s *ScenarioService) getOrCreateRequesters(proxy *url.URL) (requesters []scenarioItemRequester, err error) { From e313f1752393cb0341e113af54d091aabc1d34c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 12:26:56 +0300 Subject: [PATCH 16/32] go mod --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index d7da8751..2d3a60b2 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/jaswdr/faker v1.10.2 // indirect + github.com/libp2p/go-reuseport v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect diff --git a/go.sum b/go.sum index ffeba571..aa90e953 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jaswdr/faker v1.10.2 h1:GK03wuDqa8V6BE+2VRr3DJ/G4T0iUDCzVoBCj5TM4b8= github.com/jaswdr/faker v1.10.2/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= +github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= +github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= From 463fa69093dba4699361d8a98aa643a467acc0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 12:36:08 +0300 Subject: [PATCH 17/32] go back to client pool --- core/scenario/client_pool.go | 59 +++++---------------------------- core/scenario/requester/base.go | 2 +- core/scenario/requester/http.go | 35 +++++-------------- core/scenario/service.go | 27 ++++++--------- go.mod | 1 - go.sum | 2 -- 6 files changed, 28 insertions(+), 98 deletions(-) diff --git a/core/scenario/client_pool.go b/core/scenario/client_pool.go index 4ad28add..4892bc5e 100644 --- a/core/scenario/client_pool.go +++ b/core/scenario/client_pool.go @@ -2,13 +2,12 @@ package scenario import ( "errors" - "fmt" "net/http" ) type clientPool struct { // storage for our http.Clients - clients []chan *http.Client + clients chan *http.Client factory Factory N int } @@ -27,23 +26,16 @@ func NewClientPool(initialCap, maxCap int, factory Factory) (*clientPool, error) return nil, errors.New("invalid capacity settings") } - N := 4 - pool := &clientPool{ - clients: make([]chan *http.Client, N), + clients: make(chan *http.Client, maxCap), factory: factory, - N: N, - } - - for i := 0; i < N; i++ { - pool.clients[i] = make(chan *http.Client, maxCap/N) } // create initial clients, if something goes wrong, // just close the pool error out. for i := 0; i < initialCap; i++ { client := pool.factory() - pool.clients[i%N] <- client + pool.clients <- client } return pool, nil @@ -51,19 +43,8 @@ func NewClientPool(initialCap, maxCap int, factory Factory) (*clientPool, error) func (c *clientPool) Get() *http.Client { var client *http.Client - // TODO N = 4 select { - case client = <-c.clients[0]: - case client = <-c.clients[1]: - case client = <-c.clients[2]: - case client = <-c.clients[3]: - // case client = <-c.clients[4]: - // case client = <-c.clients[5]: - // case client = <-c.clients[6]: - // case client = <-c.clients[7]: - // case client = <-c.clients[8]: - // case client = <-c.clients[9]: - + case client = <-c.clients: default: client = c.factory() } @@ -84,27 +65,8 @@ func (c *clientPool) Put(client *http.Client) error { // put the resource back into the pool. If the pool is full, this will // block and the default case will be executed. select { - case c.clients[0] <- client: - return nil - case c.clients[1] <- client: + case c.clients <- client: return nil - case c.clients[2] <- client: - return nil - case c.clients[3] <- client: - return nil - // case c.clients[4] <- client: - // return nil - // case c.clients[5] <- client: - // return nil - // case c.clients[6] <- client: - // return nil - // case c.clients[7] <- client: - // return nil - // case c.clients[8] <- client: - // return nil - // case c.clients[9] <- client: - // return nil - default: // pool is full, close passed client client.CloseIdleConnections() @@ -117,13 +79,8 @@ func (c *clientPool) Len() int { } func (c *clientPool) Done() { - fmt.Println(c.Len()) - for i := 0; i < c.N; i++ { - close(c.clients[i]) - } - for _, cp := range c.clients { - for c := range cp { - c.CloseIdleConnections() - } + close(c.clients) + for c := range c.clients { + c.CloseIdleConnections() } } diff --git a/core/scenario/requester/base.go b/core/scenario/requester/base.go index db8a7d40..25547bac 100644 --- a/core/scenario/requester/base.go +++ b/core/scenario/requester/base.go @@ -38,7 +38,7 @@ type Requester interface { type HttpRequesterI interface { Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error - Send(client *http.Client, envs map[string]interface{}, hostPortMap map[string]string) *types.ScenarioStepResult + Send(client *http.Client, envs map[string]interface{}) *types.ScenarioStepResult } // NewRequester is the factory method of the Requester. diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index f6832729..861f65de 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -27,7 +27,6 @@ import ( "errors" "fmt" "io" - "net" "net/http" "net/http/httptrace" "net/url" @@ -38,7 +37,6 @@ import ( "time" "github.com/google/uuid" - reuse "github.com/libp2p/go-reuseport" "go.ddosify.com/ddosify/core/scenario/scripting/assertion" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/evaluator" "go.ddosify.com/ddosify/core/scenario/scripting/extraction" @@ -154,7 +152,7 @@ func (h *HttpRequester) Done() { // h.client.CloseIdleConnections() } -func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}, hostPortMap map[string]string) (res *types.ScenarioStepResult) { +func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { var statusCode int var contentLength int64 var requestErr types.RequestError @@ -180,9 +178,9 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}, h // Transport segment if client.Transport == nil { - client.Transport = h.initTransport(hostPortMap) + client.Transport = h.initTransport() } else { - h.updateTransport(client.Transport.(*http.Transport), hostPortMap) + h.updateTransport(client.Transport.(*http.Transport)) } // http client @@ -197,7 +195,7 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}, h } durations := &duration{} - trace := newTrace(durations, h.proxyAddr, h.packet.URL, hostPortMap) + trace := newTrace(durations, h.proxyAddr) httpReq, err := h.prepareReq(usableVars, trace) if err != nil { // could not prepare req @@ -456,18 +454,11 @@ func fetchErrType(err error) types.RequestError { return requestErr } -func (h *HttpRequester) initTransport(hostPortMap map[string]string) *http.Transport { +func (h *HttpRequester) initTransport() *http.Transport { tr := &http.Transport{ TLSClientConfig: h.initTLSConfig(), Proxy: http.ProxyURL(h.proxyAddr), - // MaxConnsPerHost: 1, // to use the same connection per host throughout an iteration - Dial: func(network, addr string) (net.Conn, error) { - if localAddr, ok := hostPortMap[h.packet.URL]; ok { - return reuse.Dial(network, localAddr, addr) - } else { - return net.Dial(network, addr) - } - }, + MaxConnsPerHost: 1, // to use the same connection per host throughout an iteration } tr.DisableKeepAlives = false @@ -486,19 +477,10 @@ func (h *HttpRequester) initTransport(hostPortMap map[string]string) *http.Trans return tr } -func (h *HttpRequester) updateTransport(tr *http.Transport, hostPortMap map[string]string) { +func (h *HttpRequester) updateTransport(tr *http.Transport) { tr.TLSClientConfig = h.initTLSConfig() tr.Proxy = http.ProxyURL(h.proxyAddr) - // TODO check - tr.Dial = func(network, addr string) (net.Conn, error) { - if localAddr, ok := hostPortMap[h.packet.URL]; ok { - return reuse.Dial(network, localAddr, addr) - } else { - return net.Dial(network, addr) - } - } - tr.DisableKeepAlives = false if val, ok := h.packet.Custom["keep-alive"]; ok { tr.DisableKeepAlives = !val.(bool) @@ -572,7 +554,7 @@ func (h *HttpRequester) Type() string { return "HTTP" } -func newTrace(duration *duration, proxyAddr *url.URL, reqUrl string, hostPortMap map[string]string) *httptrace.ClientTrace { +func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { var dnsStart, connStart, tlsStart, reqStart, serverProcessStart time.Time // According to the doc in the trace.go; @@ -643,7 +625,6 @@ func newTrace(duration *duration, proxyAddr *url.URL, reqUrl string, hostPortMap reqStart = time.Now() } m.Unlock() - hostPortMap[reqUrl] = connInfo.Conn.LocalAddr().String() }, WroteRequest: func(w httptrace.WroteRequestInfo) { m.Lock() diff --git a/core/scenario/service.go b/core/scenario/service.go index 237c2e70..fea4e328 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -46,8 +46,7 @@ type ScenarioService struct { // Each scenarioItem has a requester clients map[*url.URL][]scenarioItemRequester - cPool *clientPool - client *http.Client + cPool *clientPool scenario types.Scenario ctx context.Context @@ -93,15 +92,13 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi.Init() s.ei = vi - // initialClientCount := opts.MaxConcurrentIterCount - // if !opts.ConnectionReuse { - // // TODO: timeout and buffer - // initialClientCount = opts.IterationCount - // } - // maxClientCount := opts.IterationCount - // s.cPool, err = NewClientPool(initialClientCount, maxClientCount, func() *http.Client { return &http.Client{} }) - - s.client = &http.Client{} + initialClientCount := opts.MaxConcurrentIterCount + if !opts.ConnectionReuse { + // TODO: timeout and buffer + initialClientCount = opts.IterationCount + } + maxClientCount := opts.IterationCount + s.cPool, err = NewClientPool(initialClientCount, maxClientCount, func() *http.Client { return &http.Client{} }) return } @@ -132,15 +129,13 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( s.enrichEnvFromData(envs) atomic.AddInt64(&s.iterIndex, 1) - // client := s.cPool.Get() - - urlPortMap := make(map[string]string) + client := s.cPool.Get() for _, sr := range requesters { var res *types.ScenarioStepResult switch sr.requester.Type() { case "HTTP": httpRequester := sr.requester.(requester.HttpRequesterI) - res = httpRequester.Send(s.client, envs, urlPortMap) // TODO + res = httpRequester.Send(client, envs) default: res = &types.ScenarioStepResult{Err: types.RequestError{Type: fmt.Sprintf("type not defined: %s", sr.requester.Type())}} } @@ -161,7 +156,7 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( enrichEnvFromPrevStep(envs, res.ExtractedEnvs) } - // s.cPool.Put(client) + s.cPool.Put(client) return } diff --git a/go.mod b/go.mod index 2d3a60b2..d7da8751 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/jaswdr/faker v1.10.2 // indirect - github.com/libp2p/go-reuseport v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect diff --git a/go.sum b/go.sum index aa90e953..ffeba571 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jaswdr/faker v1.10.2 h1:GK03wuDqa8V6BE+2VRr3DJ/G4T0iUDCzVoBCj5TM4b8= github.com/jaswdr/faker v1.10.2/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= -github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= -github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= From c2340749e938674150c2617e19f0e8c0695cbb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 12:37:03 +0300 Subject: [PATCH 18/32] pool done --- core/scenario/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scenario/service.go b/core/scenario/service.go index fea4e328..6fa77cab 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -196,7 +196,7 @@ func (s *ScenarioService) Done() { } } - // s.cPool.Done() + s.cPool.Done() } func (s *ScenarioService) getOrCreateRequesters(proxy *url.URL) (requesters []scenarioItemRequester, err error) { From 6df492cdfa8656b5ff03ad0f68a1e09f2dc45c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 12:52:31 +0300 Subject: [PATCH 19/32] use runtime goexit for defer calls to run in benchmark --- main_benchmark_test.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/main_benchmark_test.go b/main_benchmark_test.go index ff340795..e4b26011 100644 --- a/main_benchmark_test.go +++ b/main_benchmark_test.go @@ -28,6 +28,7 @@ import ( "fmt" "log" "os" + "runtime" "runtime/pprof" "runtime/trace" "strconv" @@ -124,18 +125,22 @@ func BenchmarkEngines(t *testing.B) { i, _ := strconv.Atoi(index) conf := table[i] outSuffix := ".out" + var err error + // child proc + var cpuProfFile, memProfFile, traceFile *os.File if *cpuprofile != "" { - f, err := os.Create(fmt.Sprintf("%s_cpuprof_%s.out", strings.TrimSuffix(*cpuprofile, outSuffix), conf.name)) + cpuProfFile, err = os.Create(fmt.Sprintf("%s_cpuprof_%s.out", strings.TrimSuffix(*cpuprofile, outSuffix), conf.name)) if err != nil { log.Fatal(err) } - pprof.StartCPUProfile(f) + pprof.StartCPUProfile(cpuProfFile) + defer cpuProfFile.Close() defer pprof.StopCPUProfile() } if *memprofile != "" { // get memory profile at execution finish - memProfFile, err := os.Create(fmt.Sprintf("%s_memprof_%s.out", strings.TrimSuffix(*memprofile, outSuffix), + memProfFile, err = os.Create(fmt.Sprintf("%s_memprof_%s.out", strings.TrimSuffix(*memprofile, outSuffix), conf.name)) if err != nil { log.Fatal("could not create memory profile: ", err) @@ -150,17 +155,17 @@ func BenchmarkEngines(t *testing.B) { } if *keepTrace != "" { - f, err := os.Create(fmt.Sprintf("%s_trace_%s.out", strings.TrimSuffix(*keepTrace, outSuffix), conf.name)) + traceFile, err = os.Create(fmt.Sprintf("%s_trace_%s.out", strings.TrimSuffix(*keepTrace, outSuffix), conf.name)) if err != nil { log.Fatalf("failed to create trace output file: %v", err) } defer func() { - if err := f.Close(); err != nil { + if err := traceFile.Close(); err != nil { log.Fatalf("failed to close trace file: %v", err) } }() - if err := trace.Start(f); err != nil { + if err := trace.Start(traceFile); err != nil { log.Fatalf("failed to start trace: %v", err) } defer trace.Stop() @@ -215,7 +220,7 @@ func BenchmarkEngines(t *testing.B) { }) if !success { - os.Exit(1) + runtime.Goexit() } } } From 71f494615306765084f896d49a23375a07e52219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 16:17:32 +0300 Subject: [PATCH 20/32] add engine modes --- config/json.go | 36 ++++++++++++------------- config/json_test.go | 36 ++++++++++++------------- core/engine.go | 2 +- core/scenario/client_pool.go | 1 - core/scenario/requester/base.go | 2 +- core/scenario/requester/http.go | 38 +++++++++++++++++++------- core/scenario/service.go | 48 ++++++++++++++++++++++++--------- core/types/hammer.go | 11 +++++++- 8 files changed, 111 insertions(+), 63 deletions(-) diff --git a/config/json.go b/config/json.go index df62a70b..bcf0dea2 100644 --- a/config/json.go +++ b/config/json.go @@ -144,28 +144,28 @@ func (c *CsvConf) UnmarshalJSON(data []byte) error { } type JsonReader struct { - ReqCount *int `json:"request_count"` - IterCount *int `json:"iteration_count"` - LoadType string `json:"load_type"` - Duration int `json:"duration"` - TimeRunCount timeRunCount `json:"manual_load"` - Steps []step `json:"steps"` - Output string `json:"output"` - Proxy string `json:"proxy"` - Envs map[string]interface{} `json:"env"` - Data map[string]CsvConf `json:"data"` - Debug bool `json:"debug"` - SamplingRate *int `json:"sampling_rate"` - ConnectionReuse bool `json:"connection_reuse"` + ReqCount *int `json:"request_count"` + IterCount *int `json:"iteration_count"` + LoadType string `json:"load_type"` + Duration int `json:"duration"` + TimeRunCount timeRunCount `json:"manual_load"` + Steps []step `json:"steps"` + Output string `json:"output"` + Proxy string `json:"proxy"` + Envs map[string]interface{} `json:"env"` + Data map[string]CsvConf `json:"data"` + Debug bool `json:"debug"` + SamplingRate *int `json:"sampling_rate"` + EngineMode string `json:"engine_mode"` } func (j *JsonReader) UnmarshalJSON(data []byte) error { type jsonReaderAlias JsonReader defaultFields := &jsonReaderAlias{ - LoadType: types.DefaultLoadType, - Duration: types.DefaultDuration, - Output: types.DefaultOutputType, - ConnectionReuse: true, + LoadType: types.DefaultLoadType, + Duration: types.DefaultDuration, + Output: types.DefaultOutputType, + EngineMode: types.EngineModeDdosify, } err := json.Unmarshal(data, defaultFields) @@ -277,7 +277,7 @@ func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { ReportDestination: j.Output, Debug: j.Debug, SamplingRate: samplingRate, - ConnectionReuse: j.ConnectionReuse, + EngineMode: j.EngineMode, } return } diff --git a/config/json_test.go b/config/json_test.go index 687aed88..dbe4aa1b 100644 --- a/config/json_test.go +++ b/config/json_test.go @@ -55,8 +55,8 @@ func TestCreateHammerDefaultValues(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -111,8 +111,8 @@ func TestCreateHammer(t *testing.T) { Strategy: "single", Addr: addr, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -167,8 +167,8 @@ func TestCreateHammerWithIterationCountInsteadOfReqCount(t *testing.T) { Strategy: "single", Addr: addr, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -226,8 +226,8 @@ func TestCreateHammerWithIterationCountOverridesReqCount(t *testing.T) { Strategy: "single", Addr: addr, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -262,8 +262,8 @@ func TestCreateHammerManualLoad(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -298,8 +298,8 @@ func TestCreateHammerManualLoadOverrideOthers(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -557,8 +557,8 @@ func TestCreateHammerTLSWithOnlyCertPath(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -603,8 +603,8 @@ func TestCreateHammerTLSWithOnlyKeyPath(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() @@ -640,8 +640,8 @@ func TestCreateHammerTLSWithWithEmptyPath(t *testing.T) { Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, - SamplingRate: types.DefaultSamplingCount, - ConnectionReuse: true, + SamplingRate: types.DefaultSamplingCount, + EngineMode: types.EngineModeDdosify, } h, err := jsonReader.CreateHammer() diff --git a/core/engine.go b/core/engine.go index 35f00f72..581225ae 100644 --- a/core/engine.go +++ b/core/engine.go @@ -100,7 +100,7 @@ func (e *engine) Init() (err error) { Debug: e.hammer.Debug, IterationCount: e.hammer.IterationCount, MaxConcurrentIterCount: e.getMaxConcurrentIterCount(), - ConnectionReuse: e.hammer.ConnectionReuse, + EngineMode: e.hammer.EngineMode, }); err != nil { return } diff --git a/core/scenario/client_pool.go b/core/scenario/client_pool.go index 4892bc5e..16ba778c 100644 --- a/core/scenario/client_pool.go +++ b/core/scenario/client_pool.go @@ -9,7 +9,6 @@ type clientPool struct { // storage for our http.Clients clients chan *http.Client factory Factory - N int } // Factory is a function to create new connections. diff --git a/core/scenario/requester/base.go b/core/scenario/requester/base.go index 25547bac..57fff9e8 100644 --- a/core/scenario/requester/base.go +++ b/core/scenario/requester/base.go @@ -38,7 +38,7 @@ type Requester interface { type HttpRequesterI interface { Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error - Send(client *http.Client, envs map[string]interface{}) *types.ScenarioStepResult + Send(client *http.Client, envs map[string]interface{}) *types.ScenarioStepResult // should use its own client if client is nil } // NewRequester is the factory method of the Requester. diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 861f65de..7fcc027c 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -50,6 +50,7 @@ type HttpRequester struct { ctx context.Context proxyAddr *url.URL packet types.ScenarioStep + client *http.Client request *http.Request ei *injection.EnvironmentInjector containsDynamicField map[string]bool @@ -72,6 +73,21 @@ func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAdd h.dynamicRgx = regexp.MustCompile(regex.DynamicVariableRegex) h.envRgx = regexp.MustCompile(regex.EnvironmentVariableRegex) + // Transport segment + tr := h.initTransport() + tr.MaxConnsPerHost = 60000 + + // http client + h.client = &http.Client{Transport: tr, Timeout: time.Duration(h.packet.Timeout) * time.Second} + if val, ok := h.packet.Custom["disable-redirect"]; ok { + val := val.(bool) + if val { + h.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + } + } + // Request instance err = h.initRequestInstance() if err != nil { @@ -148,8 +164,7 @@ func (h *HttpRequester) Done() { // When the Job is finished, we have to Close idle connections to prevent sockets to lock in at the TIME_WAIT state. // Otherwise, the next job can't use these sockets because they are reserved for the current target host. - // TODO - // h.client.CloseIdleConnections() + h.client.CloseIdleConnections() } func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { @@ -173,14 +188,18 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) ( } if client == nil { - client = &http.Client{} - } - - // Transport segment - if client.Transport == nil { - client.Transport = h.initTransport() + // engine mode is 'ddosify' + // if passed client is nil , use requesters client that is dedicated to one step, thereby one transport + client = h.client } else { - h.updateTransport(client.Transport.(*http.Transport)) + // engine mode is 'distinct-user' or 'repeated-user' + // passed client is used for multiple steps throughout an iteration, update transport + if client.Transport == nil { + client.Transport = h.initTransport() + client.Transport.(*http.Transport).MaxConnsPerHost = 1 // use same connection per host throughout an iteration + } else { + h.updateTransport(client.Transport.(*http.Transport)) + } } // http client @@ -458,7 +477,6 @@ func (h *HttpRequester) initTransport() *http.Transport { tr := &http.Transport{ TLSClientConfig: h.initTLSConfig(), Proxy: http.ProxyURL(h.proxyAddr), - MaxConnsPerHost: 1, // to use the same connection per host throughout an iteration } tr.DisableKeepAlives = false diff --git a/core/scenario/service.go b/core/scenario/service.go index 6fa77cab..f26dfff9 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -53,8 +53,10 @@ type ScenarioService struct { clientMutex sync.Mutex debug bool - ei *injection.EnvironmentInjector - iterIndex int64 + engineMode string + + ei *injection.EnvironmentInjector + iterIndex int64 } // NewScenarioService is the constructor of the ScenarioService. @@ -66,7 +68,7 @@ type ScenarioOpts struct { Debug bool IterationCount int MaxConcurrentIterCount int - ConnectionReuse bool + EngineMode string } // Init initializes the ScenarioService.clients with the given types.Scenario and proxies. @@ -91,14 +93,19 @@ func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, vi := &injection.EnvironmentInjector{} vi.Init() s.ei = vi - - initialClientCount := opts.MaxConcurrentIterCount - if !opts.ConnectionReuse { - // TODO: timeout and buffer - initialClientCount = opts.IterationCount + s.engineMode = opts.EngineMode + + if s.engineInUserMode() { + // create client pool + var initialCount int + if s.engineMode == types.EngineModeRepeatedUser { + initialCount = opts.MaxConcurrentIterCount + } else if s.engineMode == types.EngineModeDistinctUser { + initialCount = opts.IterationCount + } + s.cPool, err = NewClientPool(initialCount, opts.IterationCount, func() *http.Client { return &http.Client{} }) } - maxClientCount := opts.IterationCount - s.cPool, err = NewClientPool(initialClientCount, maxClientCount, func() *http.Client { return &http.Client{} }) + // s.cPool will be nil otherwise return } @@ -129,7 +136,13 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( s.enrichEnvFromData(envs) atomic.AddInt64(&s.iterIndex, 1) - client := s.cPool.Get() + var client *http.Client + if s.engineInUserMode() { + // get client from pool + client = s.cPool.Get() + defer s.cPool.Put(client) + } + for _, sr := range requesters { var res *types.ScenarioStepResult switch sr.requester.Type() { @@ -156,7 +169,7 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( enrichEnvFromPrevStep(envs, res.ExtractedEnvs) } - s.cPool.Put(client) + return } @@ -166,6 +179,13 @@ func enrichEnvFromPrevStep(m1 map[string]interface{}, m2 map[string]interface{}) } } +func (s *ScenarioService) engineInUserMode() bool { + if s.engineMode == types.EngineModeDistinctUser || s.engineMode == types.EngineModeRepeatedUser { + return true + } + return false +} + func (s *ScenarioService) enrichEnvFromData(envs map[string]interface{}) { var row map[string]interface{} sb := strings.Builder{} @@ -196,7 +216,9 @@ func (s *ScenarioService) Done() { } } - s.cPool.Done() + if s.cPool != nil { + s.cPool.Done() + } } func (s *ScenarioService) getOrCreateRequesters(proxy *url.URL) (requesters []scenarioItemRequester, err error) { diff --git a/core/types/hammer.go b/core/types/hammer.go index 86417665..be68c051 100644 --- a/core/types/hammer.go +++ b/core/types/hammer.go @@ -35,6 +35,11 @@ const ( LoadTypeIncremental = "incremental" LoadTypeWaved = "waved" + // EngineModes + EngineModeDistinctUser = "distinct-user" + EngineModeRepeatedUser = "repeated-user" + EngineModeDdosify = "ddosify" + // Default Values DefaultIterCount = 100 DefaultLoadType = LoadTypeLinear @@ -46,6 +51,7 @@ const ( ) var loadTypes = [...]string{LoadTypeLinear, LoadTypeIncremental, LoadTypeWaved} +var engineModes = [...]string{EngineModeDdosify, EngineModeDistinctUser, EngineModeRepeatedUser} // TimeRunCount is the data structure to store manual load type data. type TimeRunCount []struct { @@ -87,7 +93,7 @@ type Hammer struct { SamplingRate int // Connection reuse - ConnectionReuse bool + EngineMode string } // Validate validates attack metadata and executes the validation methods of the services. @@ -101,6 +107,9 @@ func (h *Hammer) Validate() error { if h.LoadType != "" && !util.StringInSlice(h.LoadType, loadTypes[:]) { return fmt.Errorf("unsupported LoadType: %s", h.LoadType) } + if h.EngineMode != "" && !util.StringInSlice(h.EngineMode, engineModes[:]) { + return fmt.Errorf("unsupported EngineMode: %s", h.EngineMode) + } if len(h.TimeRunCountMap) > 0 { for _, t := range h.TimeRunCountMap { From 01202603e95dcf7a7d85fe055b6d0f9a3155a4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 16:29:36 +0300 Subject: [PATCH 21/32] fix unnecessary race --- core/scenario/requester/http.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 7fcc027c..04c25a9a 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -202,17 +202,6 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) ( } } - // http client - client.Timeout = time.Duration(h.packet.Timeout) * time.Second - if val, ok := h.packet.Custom["disable-redirect"]; ok { - val := val.(bool) - if val { - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - } - } - durations := &duration{} trace := newTrace(durations, h.proxyAddr) httpReq, err := h.prepareReq(usableVars, trace) From a9432e7ae202e9c85ad2ec7a61c87755275a7454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 16:37:09 +0300 Subject: [PATCH 22/32] fix test --- core/scenario/requester/http_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/scenario/requester/http_test.go b/core/scenario/requester/http_test.go index 16e9627a..c48e899e 100644 --- a/core/scenario/requester/http_test.go +++ b/core/scenario/requester/http_test.go @@ -158,10 +158,8 @@ func TestInitClient(t *testing.T) { tf := func(t *testing.T) { h := &HttpRequester{} h.Init(test.ctx, test.scenarioItem, test.proxy, false, nil) - client := &http.Client{} - h.Send(client, map[string]interface{}{}) - transport := client.Transport.(*http.Transport) + transport := h.client.Transport.(*http.Transport) tls := transport.TLSClientConfig // TLS Assert (Also check HTTP2 vs HTTP) @@ -189,11 +187,11 @@ func TestInitClient(t *testing.T) { } // Client Assert - if test.client.Timeout != client.Timeout { - t.Errorf("Timeout Expected %v, Found %v", test.client.Timeout, client.Timeout) + if test.client.Timeout != h.client.Timeout { + t.Errorf("Timeout Expected %v, Found %v", test.client.Timeout, h.client.Timeout) } - crFunc := client.CheckRedirect == nil + crFunc := h.client.CheckRedirect == nil expectedCRFunc := test.client.CheckRedirect == nil if expectedCRFunc != crFunc { t.Errorf("CheckRedirect Expected %v, Found %v", expectedCRFunc, crFunc) From 7889ff89a9094356715f212802703becee38143a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 16:51:33 +0300 Subject: [PATCH 23/32] run benchmarks on repeated-user mode --- config/config_testdata/benchmark/config_correlation_load_1.json | 1 + config/config_testdata/benchmark/config_correlation_load_2.json | 1 + config/config_testdata/benchmark/config_correlation_load_3.json | 1 + config/config_testdata/benchmark/config_correlation_load_4.json | 2 ++ config/config_testdata/benchmark/config_correlation_load_5.json | 2 ++ 5 files changed, 7 insertions(+) diff --git a/config/config_testdata/benchmark/config_correlation_load_1.json b/config/config_testdata/benchmark/config_correlation_load_1.json index 375adf37..3a959f8e 100644 --- a/config/config_testdata/benchmark/config_correlation_load_1.json +++ b/config/config_testdata/benchmark/config_correlation_load_1.json @@ -1,5 +1,6 @@ { "iteration_count": 100, + "engine_mode": "repeated-user", "load_type": "waved", "duration": 10, "steps": [ diff --git a/config/config_testdata/benchmark/config_correlation_load_2.json b/config/config_testdata/benchmark/config_correlation_load_2.json index 785776dd..8ccf276c 100644 --- a/config/config_testdata/benchmark/config_correlation_load_2.json +++ b/config/config_testdata/benchmark/config_correlation_load_2.json @@ -1,6 +1,7 @@ { "iteration_count": 1000, "load_type": "waved", + "engine_mode": "repeated-user", "duration": 10, "steps": [ { diff --git a/config/config_testdata/benchmark/config_correlation_load_3.json b/config/config_testdata/benchmark/config_correlation_load_3.json index c4be1283..d16873a1 100644 --- a/config/config_testdata/benchmark/config_correlation_load_3.json +++ b/config/config_testdata/benchmark/config_correlation_load_3.json @@ -1,6 +1,7 @@ { "iteration_count": 5000, "load_type": "waved", + "engine_mode": "repeated-user", "duration": 10, "steps": [ { diff --git a/config/config_testdata/benchmark/config_correlation_load_4.json b/config/config_testdata/benchmark/config_correlation_load_4.json index ca1fbb0f..4b660e93 100644 --- a/config/config_testdata/benchmark/config_correlation_load_4.json +++ b/config/config_testdata/benchmark/config_correlation_load_4.json @@ -1,6 +1,8 @@ { "iteration_count": 10000, "load_type": "waved", + "engine_mode": "repeated-user", + "duration": 10, "steps": [ { diff --git a/config/config_testdata/benchmark/config_correlation_load_5.json b/config/config_testdata/benchmark/config_correlation_load_5.json index c38f24d2..7919b1b5 100644 --- a/config/config_testdata/benchmark/config_correlation_load_5.json +++ b/config/config_testdata/benchmark/config_correlation_load_5.json @@ -1,6 +1,8 @@ { "iteration_count": 20000, "load_type": "waved", + "engine_mode": "repeated-user", + "duration": 10, "steps": [ { From cefcca99cfa00029956de3c22692074025a14354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 17:43:30 +0300 Subject: [PATCH 24/32] run benchmarks on ddosify mode --- config/config_testdata/benchmark/config_correlation_load_1.json | 2 +- config/config_testdata/benchmark/config_correlation_load_2.json | 2 +- config/config_testdata/benchmark/config_correlation_load_3.json | 2 +- config/config_testdata/benchmark/config_correlation_load_4.json | 2 +- config/config_testdata/benchmark/config_correlation_load_5.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/config_testdata/benchmark/config_correlation_load_1.json b/config/config_testdata/benchmark/config_correlation_load_1.json index 3a959f8e..16984586 100644 --- a/config/config_testdata/benchmark/config_correlation_load_1.json +++ b/config/config_testdata/benchmark/config_correlation_load_1.json @@ -1,6 +1,6 @@ { "iteration_count": 100, - "engine_mode": "repeated-user", + "engine_mode": "ddosify", "load_type": "waved", "duration": 10, "steps": [ diff --git a/config/config_testdata/benchmark/config_correlation_load_2.json b/config/config_testdata/benchmark/config_correlation_load_2.json index 8ccf276c..15b4c8b2 100644 --- a/config/config_testdata/benchmark/config_correlation_load_2.json +++ b/config/config_testdata/benchmark/config_correlation_load_2.json @@ -1,7 +1,7 @@ { "iteration_count": 1000, "load_type": "waved", - "engine_mode": "repeated-user", + "engine_mode": "ddosify", "duration": 10, "steps": [ { diff --git a/config/config_testdata/benchmark/config_correlation_load_3.json b/config/config_testdata/benchmark/config_correlation_load_3.json index d16873a1..0e9343ec 100644 --- a/config/config_testdata/benchmark/config_correlation_load_3.json +++ b/config/config_testdata/benchmark/config_correlation_load_3.json @@ -1,7 +1,7 @@ { "iteration_count": 5000, "load_type": "waved", - "engine_mode": "repeated-user", + "engine_mode": "ddosify", "duration": 10, "steps": [ { diff --git a/config/config_testdata/benchmark/config_correlation_load_4.json b/config/config_testdata/benchmark/config_correlation_load_4.json index 4b660e93..f386f549 100644 --- a/config/config_testdata/benchmark/config_correlation_load_4.json +++ b/config/config_testdata/benchmark/config_correlation_load_4.json @@ -1,7 +1,7 @@ { "iteration_count": 10000, "load_type": "waved", - "engine_mode": "repeated-user", + "engine_mode": "ddosify", "duration": 10, "steps": [ diff --git a/config/config_testdata/benchmark/config_correlation_load_5.json b/config/config_testdata/benchmark/config_correlation_load_5.json index 7919b1b5..d3179055 100644 --- a/config/config_testdata/benchmark/config_correlation_load_5.json +++ b/config/config_testdata/benchmark/config_correlation_load_5.json @@ -1,7 +1,7 @@ { "iteration_count": 20000, "load_type": "waved", - "engine_mode": "repeated-user", + "engine_mode": "ddosify", "duration": 10, "steps": [ From b23afa58fb7966c69e371bc217f5974fee971ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 17:45:22 +0300 Subject: [PATCH 25/32] set maxIdleConns to unlimited --- core/scenario/requester/http.go | 1 + 1 file changed, 1 insertion(+) diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 04c25a9a..ad4772c3 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -76,6 +76,7 @@ func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAdd // Transport segment tr := h.initTransport() tr.MaxConnsPerHost = 60000 + tr.MaxIdleConns = 0 // http client h.client = &http.Client{Transport: tr, Timeout: time.Duration(h.packet.Timeout) * time.Second} From 5086a65862ab96ff254a9448cabc0ad223ddcb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 17 Feb 2023 18:09:19 +0300 Subject: [PATCH 26/32] change maxidle conns per host --- core/scenario/requester/http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index ad4772c3..342685f7 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -75,7 +75,7 @@ func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAdd // Transport segment tr := h.initTransport() - tr.MaxConnsPerHost = 60000 + tr.MaxIdleConnsPerHost = 60000 tr.MaxIdleConns = 0 // http client From 7816b7b70bad24eec941366df0fb1a98697b2455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Mon, 20 Feb 2023 15:41:39 +0300 Subject: [PATCH 27/32] put headers added by client to step result --- core/scenario/requester/http.go | 24 +++++++++++++++++++++--- core/scenario/requester/http_test.go | 11 +++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 342685f7..ca5d59bb 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -204,7 +204,8 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) ( } durations := &duration{} - trace := newTrace(durations, h.proxyAddr) + headersAddedByClient := make(map[string][]string) + trace := newTrace(durations, h.proxyAddr, headersAddedByClient) httpReq, err := h.prepareReq(usableVars, trace) if err != nil { // could not prepare req @@ -291,7 +292,7 @@ func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) ( Url: httpReq.URL.String(), Method: httpReq.Method, - ReqHeaders: httpReq.Header, + ReqHeaders: concatHeaders(httpReq.Header, headersAddedByClient), ReqBody: copiedReqBody.Bytes(), RespHeaders: respHeaders, RespBody: respBody, @@ -334,6 +335,20 @@ func concatEnvs(envs1, envs2 map[string]interface{}) map[string]interface{} { return total } +func concatHeaders(envs1, envs2 map[string][]string) map[string][]string { + total := make(map[string][]string) + + for k, v := range envs1 { + total[k] = v + } + + for k, v := range envs2 { + total[k] = v + } + + return total +} + func (h *HttpRequester) prepareReq(envs map[string]interface{}, trace *httptrace.ClientTrace) (*http.Request, error) { re := regexp.MustCompile(regex.DynamicVariableRegex) httpReq := h.request.Clone(h.ctx) @@ -562,7 +577,7 @@ func (h *HttpRequester) Type() string { return "HTTP" } -func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { +func newTrace(duration *duration, proxyAddr *url.URL, headersByClient map[string][]string) *httptrace.ClientTrace { var dnsStart, connStart, tlsStart, reqStart, serverProcessStart time.Time // According to the doc in the trace.go; @@ -649,6 +664,9 @@ func newTrace(duration *duration, proxyAddr *url.URL) *httptrace.ClientTrace { duration.setResStartTime(time.Now()) m.Unlock() }, + WroteHeaderField: func(key string, value []string) { + headersByClient[key] = value + }, } } diff --git a/core/scenario/requester/http_test.go b/core/scenario/requester/http_test.go index c48e899e..d1ee92a7 100644 --- a/core/scenario/requester/http_test.go +++ b/core/scenario/requester/http_test.go @@ -379,11 +379,14 @@ func TestSendOnDebugModePopulatesDebugInfo(t *testing.T) { t.Errorf("RequestBody Expected %#v, Found: \n%#v", expectedRequestBody, res.ReqBody) } - if !reflect.DeepEqual(expectedRequestHeaders, res.ReqHeaders) { - t.Errorf("RequestHeaders Expected %#v, Found: \n%#v", expectedRequestHeaders, - res.ReqHeaders) - } + // stepResult has default request headers added by go client + for expKey, expVal := range expectedRequestHeaders { + if !reflect.DeepEqual(expVal, res.ReqHeaders.Values(expKey)) { + t.Errorf("RequestHeaders Expected %#v, Found: \n%#v", expectedRequestHeaders, + res.ReqHeaders) + } + } } t.Run("populate-debug-info", tf) } From b9da7d2d587d6481d4ffc77e22e0778f2566c77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 22 Feb 2023 10:02:12 +0000 Subject: [PATCH 28/32] add test for engine modes --- core/engine_test.go | 154 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/core/engine_test.go b/core/engine_test.go index 634d7643..e789fa54 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -1772,6 +1772,160 @@ func TestTLSMutualAuthButServerAndClientHasDifferentCerts(t *testing.T) { } } +func TestEngineModeUserKeepAlive(t *testing.T) { + t.Parallel() + // For DistinctUser and RepeatedUser modes + + // Test server + clientAddress1 := []string{} + clientAddress2 := []string{} + var m1 sync.Mutex + var m2 sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m1.Lock() + defer m1.Unlock() + + clientAddress1 = append(clientAddress1, r.RemoteAddr) // network address that sent the request + } + + secondReqHandler := func(w http.ResponseWriter, r *http.Request) { + m2.Lock() + defer m2.Unlock() + + clientAddress2 = append(clientAddress2, r.RemoteAddr) // network address that sent the request + } + + pathFirst := "/first" + pathSecond := "/second" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + mux.HandleFunc(pathSecond, secondReqHandler) + + host := httptest.NewServer(mux) + defer host.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 2 + h.Scenario.Steps = make([]types.ScenarioStep, 2) + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host.URL + pathFirst, + Headers: map[string]string{"ID": "1"}, + } + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host.URL + pathSecond, + Headers: map[string]string{"ID": "2"}, + } + + // Act + h.EngineMode = types.EngineModeDistinctUser + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeDistinctUserKeepAlive error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeDistinctUserKeepAlive error occurred %v", err) + } + + e.Start() + + // same host + + // check first iter + if clientAddress1[0] != clientAddress2[0] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, same hosts connection should be same throughout iteration") + } + // check second iter + if clientAddress1[1] != clientAddress2[1] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, same hosts connection should be same throughout iteration") + } + +} + +func TestEngineModeDdosifyKeepAlive(t *testing.T) { + t.Parallel() + + // Test server + clientAddress1 := []string{} + clientAddress2 := []string{} + var m1 sync.Mutex + var m2 sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m1.Lock() + defer m1.Unlock() + + clientAddress1 = append(clientAddress1, r.RemoteAddr) // network address that sent the request + } + + secondReqHandler := func(w http.ResponseWriter, r *http.Request) { + m2.Lock() + defer m2.Unlock() + + clientAddress2 = append(clientAddress2, r.RemoteAddr) // network address that sent the request + } + + pathFirst := "/first" + pathSecond := "/second" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + mux.HandleFunc(pathSecond, secondReqHandler) + + host := httptest.NewServer(mux) + defer host.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 2 + h.Scenario.Steps = make([]types.ScenarioStep, 2) + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host.URL + pathFirst, + Headers: map[string]string{"ID": "1"}, + } + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host.URL + pathSecond, + Headers: map[string]string{"ID": "2"}, + } + + // Act + h.EngineMode = types.EngineModeDdosify + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeDdosifyKeepAlive error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeDdosifyKeepAlive error occurred %v", err) + } + + e.Start() + + // same host + // in ddosify mode every step has its own client, therefore connections should be different + // check first iter + if clientAddress1[0] == clientAddress2[0] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, ") + } + // check second iter + if clientAddress1[1] == clientAddress2[1] { + t.Errorf("TestEngineModeDistinctUserKeepAlive, ") + } + +} func createCertPairFiles(cert string, certKey string) (*os.File, *os.File, error) { certFile, err := os.CreateTemp("", ".pem") if err != nil { From 1b407ccc108eeed7e3a9f1a0b61d9423feb2eac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 22 Feb 2023 11:05:52 +0000 Subject: [PATCH 29/32] engine mode tests --- core/engine_test.go | 171 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 154 insertions(+), 17 deletions(-) diff --git a/core/engine_test.go b/core/engine_test.go index e789fa54..2695465b 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -1811,20 +1811,18 @@ func TestEngineModeUserKeepAlive(t *testing.T) { h.IterationCount = 2 h.Scenario.Steps = make([]types.ScenarioStep, 2) h.Scenario.Steps[0] = types.ScenarioStep{ - ID: 1, - Method: "GET", - URL: host.URL + pathFirst, - Headers: map[string]string{"ID": "1"}, + ID: 1, + Method: "GET", + URL: host.URL + pathFirst, } h.Scenario.Steps[1] = types.ScenarioStep{ - ID: 2, - Method: "GET", - URL: host.URL + pathSecond, - Headers: map[string]string{"ID": "2"}, + ID: 2, + Method: "GET", + URL: host.URL + pathSecond, } // Act - h.EngineMode = types.EngineModeDistinctUser + h.EngineMode = types.EngineModeRepeatedUser // could have been DistinctUser also e, err := NewEngine(context.TODO(), h) if err != nil { t.Errorf("TestEngineModeDistinctUserKeepAlive error occurred %v", err) @@ -1850,6 +1848,147 @@ func TestEngineModeUserKeepAlive(t *testing.T) { } +func TestEngineModeUserKeepAliveDifferentHosts(t *testing.T) { + t.Parallel() + // For DistinctUser and RepeatedUser modes + + // Test server + clientAddress := make(map[string]struct{}) + var m sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m.Lock() + defer m.Unlock() + clientAddress[r.RemoteAddr] = struct{}{} // network address that sent the request + } + + pathFirst := "/first" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + + host1 := httptest.NewServer(mux) + host2 := httptest.NewServer(mux) + + defer host1.Close() + defer host2.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 1 + h.Scenario.Steps = make([]types.ScenarioStep, 4) + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[2] = types.ScenarioStep{ + ID: 3, + Method: "GET", + URL: host2.URL + pathFirst, + } + h.Scenario.Steps[3] = types.ScenarioStep{ + ID: 4, + Method: "GET", + URL: host2.URL + pathFirst, + } + + // Act + h.EngineMode = types.EngineModeDistinctUser // could have been RepeatedUser also + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + e.Start() + + // one iteration, two hosts, two connections expected + if len(clientAddress) != 2 { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts, expected 2 connections, got : %d", len(clientAddress)) + } +} + +func TestEngineModeUserKeepAlive_StepsKeepAliveFalse(t *testing.T) { + t.Parallel() + // For DistinctUser and RepeatedUser modes + // Test server + clientAddress := make(map[string]struct{}) + var m sync.Mutex + + firstReqHandler := func(w http.ResponseWriter, r *http.Request) { + m.Lock() + defer m.Unlock() + clientAddress[r.RemoteAddr] = struct{}{} // network address that sent the request + } + + pathFirst := "/first" + + mux := http.NewServeMux() + mux.HandleFunc(pathFirst, firstReqHandler) + + host1 := httptest.NewServer(mux) + + defer host1.Close() + + // Prepare + h := newDummyHammer() + h.IterationCount = 1 + h.Scenario.Steps = make([]types.ScenarioStep, 4) + // connection opened by 1 will not be reused + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: host1.URL + pathFirst, + Custom: map[string]interface{}{"keep-alive": false}, + } + // below will use the connection opened by 2 + h.Scenario.Steps[1] = types.ScenarioStep{ + ID: 2, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[2] = types.ScenarioStep{ + ID: 3, + Method: "GET", + URL: host1.URL + pathFirst, + } + h.Scenario.Steps[3] = types.ScenarioStep{ + ID: 4, + Method: "GET", + URL: host1.URL + pathFirst, + } + + // Act + h.EngineMode = types.EngineModeDistinctUser + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) + } + + e.Start() + + // one iteration, one host, 4 steps, one's keep-alive is false + if len(clientAddress) != 2 { + t.Errorf("TestEngineModeUserKeepAliveDifferentHosts, expected 2 connections, got : %d", len(clientAddress)) + } + +} + func TestEngineModeDdosifyKeepAlive(t *testing.T) { t.Parallel() @@ -1888,16 +2027,14 @@ func TestEngineModeDdosifyKeepAlive(t *testing.T) { h.IterationCount = 2 h.Scenario.Steps = make([]types.ScenarioStep, 2) h.Scenario.Steps[0] = types.ScenarioStep{ - ID: 1, - Method: "GET", - URL: host.URL + pathFirst, - Headers: map[string]string{"ID": "1"}, + ID: 1, + Method: "GET", + URL: host.URL + pathFirst, } h.Scenario.Steps[1] = types.ScenarioStep{ - ID: 2, - Method: "GET", - URL: host.URL + pathSecond, - Headers: map[string]string{"ID": "2"}, + ID: 2, + Method: "GET", + URL: host.URL + pathSecond, } // Act From 5be3c0ce0c1d0fce6edd73b14cc0f168c4dc8ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 22 Feb 2023 11:21:31 +0000 Subject: [PATCH 30/32] add engine mode to config json --- config_examples/config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config_examples/config.json b/config_examples/config.json index a642db5b..6df22987 100644 --- a/config_examples/config.json +++ b/config_examples/config.json @@ -4,6 +4,7 @@ "iteration_count": 30, "debug" : false, // use this field for debugging, see verbose result "load_type": "linear", + "engine_mode": "distinct-user", // could be one of "distinct-user","repeated-user", or default mode "ddosify" "duration": 5, "manual_load": [ {"duration": 5, "count": 5}, From 18ae153745d95dc555d9254436a6a24cf6c1bdef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 22 Feb 2023 12:29:59 +0000 Subject: [PATCH 31/32] add engine mode to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index f7f24890..2ddcc7d1 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,12 @@ There is an example config file at [config_examples/config.json](/config_example - `output` *optional* This is the equivalent of the `-o` flag. +- `engine_mode` *optional* + Can be one of `distinct-user`, `repeated-user`, or default mode `ddosify`. + - `distinct-user` mode simulates a new user for every iteration. + - `repeated-user` mode can use pre-used user in subsequent iterations. + - `ddosify` mode is default mode of the engine. In this mode engine runs in its max capacity, and does not show user simulation behaviour. + - `env` *optional* Scenario-scoped global variables. Note that dynamic variables changes every iteration. ```json @@ -282,6 +288,7 @@ There is an example config file at [config_examples/config.json](/config_example "randomCountry" : "{{_randomCountry}}" } ``` + - `data` *optional* Config for loading test data from a CSV file. [CSV data](https://github.com/ddosify/ddosify/tree/master/config/config_testdata/test.csv) used in below config. From fd3d1fdfce3bf8e1f00b5d3557e0aad9d55c41b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 24 Feb 2023 12:15:57 +0000 Subject: [PATCH 32/32] remove keep-alive from others, look for Connection header instead --- README.md | 3 +-- .../benchmark/config_correlation_load_1.json | 3 --- .../benchmark/config_correlation_load_2.json | 3 --- .../benchmark/config_correlation_load_3.json | 3 --- .../benchmark/config_correlation_load_4.json | 3 --- .../benchmark/config_correlation_load_5.json | 3 --- config/config_testdata/config.json | 1 - config/config_testdata/config_data_csv.json | 1 - config/config_testdata/config_debug_false.json | 1 - config/config_testdata/config_debug_mode.json | 1 - config/config_testdata/config_env_vars.json | 1 - config/config_testdata/config_incorrect.json | 2 -- config/config_testdata/config_iteration_count.json | 1 - .../config_iteration_count_over_req_count.json | 1 - config/json_test.go | 12 +++--------- config_examples/config.json | 1 - core/engine_test.go | 10 +++++----- core/scenario/requester/http.go | 12 ++++++------ core/scenario/requester/http_test.go | 11 +++++------ 19 files changed, 20 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 2ddcc7d1..6e20aafb 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ The features you can use by config file; - Custom load type creation - Payload from a file - Multipart/form-data payload -- Extra connection configuration, like *keep-alive* enable/disable logic +- Extra connection configuration - HTTP2 support @@ -501,7 +501,6 @@ There is an example config file at [config_examples/config.json](/config_example ```json "others": { - "keep-alive": true, // Default true "disable-compression": false, // Default true "h2": true, // Enables HTTP/2. Default false. "disable-redirect": true // Default false diff --git a/config/config_testdata/benchmark/config_correlation_load_1.json b/config/config_testdata/benchmark/config_correlation_load_1.json index 16984586..9a969a0b 100644 --- a/config/config_testdata/benchmark/config_correlation_load_1.json +++ b/config/config_testdata/benchmark/config_correlation_load_1.json @@ -11,7 +11,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -37,7 +36,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -56,7 +54,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_2.json b/config/config_testdata/benchmark/config_correlation_load_2.json index 15b4c8b2..f62bc132 100644 --- a/config/config_testdata/benchmark/config_correlation_load_2.json +++ b/config/config_testdata/benchmark/config_correlation_load_2.json @@ -11,7 +11,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -37,7 +36,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -56,7 +54,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_3.json b/config/config_testdata/benchmark/config_correlation_load_3.json index 0e9343ec..f4d78694 100644 --- a/config/config_testdata/benchmark/config_correlation_load_3.json +++ b/config/config_testdata/benchmark/config_correlation_load_3.json @@ -11,7 +11,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -37,7 +36,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -56,7 +54,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_4.json b/config/config_testdata/benchmark/config_correlation_load_4.json index f386f549..dd2908c7 100644 --- a/config/config_testdata/benchmark/config_correlation_load_4.json +++ b/config/config_testdata/benchmark/config_correlation_load_4.json @@ -12,7 +12,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -38,7 +37,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -57,7 +55,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/benchmark/config_correlation_load_5.json b/config/config_testdata/benchmark/config_correlation_load_5.json index d3179055..f051bab8 100644 --- a/config/config_testdata/benchmark/config_correlation_load_5.json +++ b/config/config_testdata/benchmark/config_correlation_load_5.json @@ -12,7 +12,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -38,7 +37,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, @@ -57,7 +55,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/config.json b/config/config_testdata/config.json index e223fb95..6ace2b82 100644 --- a/config/config_testdata/config.json +++ b/config/config_testdata/config.json @@ -12,7 +12,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_data_csv.json b/config/config_testdata/config_data_csv.json index 13b6881d..24d5bbf8 100644 --- a/config/config_testdata/config_data_csv.json +++ b/config/config_testdata/config_data_csv.json @@ -10,7 +10,6 @@ "method": "GET", "others": { "h2": false, - "keep-alive": true, "disable-redirect": true, "disable-compression": false }, diff --git a/config/config_testdata/config_debug_false.json b/config/config_testdata/config_debug_false.json index 92e18baa..874d8d7a 100644 --- a/config/config_testdata/config_debug_false.json +++ b/config/config_testdata/config_debug_false.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_debug_mode.json b/config/config_testdata/config_debug_mode.json index 1e661c64..08ed4adc 100644 --- a/config/config_testdata/config_debug_mode.json +++ b/config/config_testdata/config_debug_mode.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_env_vars.json b/config/config_testdata/config_env_vars.json index 93c5a7ea..6b670b6b 100644 --- a/config/config_testdata/config_env_vars.json +++ b/config/config_testdata/config_env_vars.json @@ -12,7 +12,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true }, "capture_env": { "ENV_VAR1" :{"json_path":""}, diff --git a/config/config_testdata/config_incorrect.json b/config/config_testdata/config_incorrect.json index f31b65d8..c1d17c24 100644 --- a/config/config_testdata/config_incorrect.json +++ b/config/config_testdata/config_incorrect.json @@ -19,7 +19,6 @@ "payload": "body yt kanl adnlandlandaln", "timeout": 1, "others": { - "keep-alive": true } }, { @@ -33,7 +32,6 @@ "payload_file": "config_examples/payload.txt", "timeout": 1, "others": { - "keep-alive": false } }, ], diff --git a/config/config_testdata/config_iteration_count.json b/config/config_testdata/config_iteration_count.json index 54dc3612..e33a75a3 100644 --- a/config/config_testdata/config_iteration_count.json +++ b/config/config_testdata/config_iteration_count.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/config_testdata/config_iteration_count_over_req_count.json b/config/config_testdata/config_iteration_count_over_req_count.json index bb6470f5..6e71eea8 100644 --- a/config/config_testdata/config_iteration_count_over_req_count.json +++ b/config/config_testdata/config_iteration_count_over_req_count.json @@ -13,7 +13,6 @@ "timeout": 3, "sleep": "1000", "others": { - "keep-alive": true } }, { diff --git a/config/json_test.go b/config/json_test.go index db4ef975..30718b59 100644 --- a/config/json_test.go +++ b/config/json_test.go @@ -90,9 +90,7 @@ func TestCreateHammer(t *testing.T) { Timeout: 3, Sleep: "1000", Payload: "payload str", - Custom: map[string]interface{}{ - "keep-alive": true, - }, + Custom: map[string]interface{}{}, }, { ID: 2, @@ -147,9 +145,7 @@ func TestCreateHammerWithIterationCountInsteadOfReqCount(t *testing.T) { Timeout: 3, Sleep: "1000", Payload: "payload str", - Custom: map[string]interface{}{ - "keep-alive": true, - }, + Custom: map[string]interface{}{}, }, { ID: 2, @@ -206,9 +202,7 @@ func TestCreateHammerWithIterationCountOverridesReqCount(t *testing.T) { Timeout: 3, Sleep: "1000", Payload: "payload str", - Custom: map[string]interface{}{ - "keep-alive": true, - }, + Custom: map[string]interface{}{}, }, { ID: 2, diff --git a/config_examples/config.json b/config_examples/config.json index 6df22987..01eafeb5 100644 --- a/config_examples/config.json +++ b/config_examples/config.json @@ -55,7 +55,6 @@ "password": "12345" }, "others": { - "keep-alive": true, "disable-compression": false, "h2": true, "disable-redirect": true diff --git a/core/engine_test.go b/core/engine_test.go index 2695465b..9830f7db 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -1946,10 +1946,10 @@ func TestEngineModeUserKeepAlive_StepsKeepAliveFalse(t *testing.T) { h.Scenario.Steps = make([]types.ScenarioStep, 4) // connection opened by 1 will not be reused h.Scenario.Steps[0] = types.ScenarioStep{ - ID: 1, - Method: "GET", - URL: host1.URL + pathFirst, - Custom: map[string]interface{}{"keep-alive": false}, + ID: 1, + Method: "GET", + URL: host1.URL + pathFirst, + Headers: map[string]string{"Connection": "close"}, } // below will use the connection opened by 2 h.Scenario.Steps[1] = types.ScenarioStep{ @@ -1982,7 +1982,7 @@ func TestEngineModeUserKeepAlive_StepsKeepAliveFalse(t *testing.T) { e.Start() - // one iteration, one host, 4 steps, one's keep-alive is false + // one iteration, one host, 4 steps, one's keep-alive is false (Connection: close) if len(clientAddress) != 2 { t.Errorf("TestEngineModeUserKeepAliveDifferentHosts, expected 2 connections, got : %d", len(clientAddress)) } diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index ca5d59bb..5ed359a9 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -485,8 +485,8 @@ func (h *HttpRequester) initTransport() *http.Transport { } tr.DisableKeepAlives = false - if val, ok := h.packet.Custom["keep-alive"]; ok { - tr.DisableKeepAlives = !val.(bool) + if h.packet.Headers["Connection"] == "close" { + tr.DisableKeepAlives = true } if val, ok := h.packet.Custom["disable-compression"]; ok { tr.DisableCompression = val.(bool) @@ -505,8 +505,8 @@ func (h *HttpRequester) updateTransport(tr *http.Transport) { tr.Proxy = http.ProxyURL(h.proxyAddr) tr.DisableKeepAlives = false - if val, ok := h.packet.Custom["keep-alive"]; ok { - tr.DisableKeepAlives = !val.(bool) + if h.packet.Headers["Connection"] == "close" { + tr.DisableKeepAlives = true } if val, ok := h.packet.Custom["disable-compression"]; ok { tr.DisableCompression = val.(bool) @@ -567,8 +567,8 @@ func (h *HttpRequester) initRequestInstance() (err error) { // If keep-alive is false, prevent the reuse of the previous TCP connection at the request layer also. h.request.Close = false - if val, ok := h.packet.Custom["keep-alive"]; ok { - h.request.Close = !val.(bool) + if h.packet.Headers["Connection"] == "close" { + h.request.Close = true } return } diff --git a/core/scenario/requester/http_test.go b/core/scenario/requester/http_test.go index d1ee92a7..b661d6bc 100644 --- a/core/scenario/requester/http_test.go +++ b/core/scenario/requester/http_test.go @@ -90,9 +90,9 @@ func TestInitClient(t *testing.T) { Method: http.MethodGet, URL: "https://test.com", Timeout: types.DefaultTimeout, + Headers: map[string]string{"Connection": "close"}, Custom: map[string]interface{}{ "disable-redirect": true, - "keep-alive": false, "disable-compression": true, "hostname": "dummy.com", }, @@ -279,11 +279,9 @@ func TestInitRequest(t *testing.T) { Password: "123", }, Headers: map[string]string{ - "Header1": "Value1", - "Header2": "Value2", - }, - Custom: map[string]interface{}{ - "keep-alive": false, + "Header1": "Value1", + "Header2": "Value2", + "Connection": "close", }, } expectedWithoutKeepAlive, _ := http.NewRequest(sWithoutKeepAlive.Method, @@ -292,6 +290,7 @@ func TestInitRequest(t *testing.T) { expectedWithoutKeepAlive.Header = make(http.Header) expectedWithoutKeepAlive.Header.Set("Header1", "Value1") expectedWithoutKeepAlive.Header.Set("Header2", "Value2") + expectedWithoutKeepAlive.Header.Set("Connection", "close") expectedWithoutKeepAlive.SetBasicAuth(sWithoutKeepAlive.Auth.Username, sWithoutKeepAlive.Auth.Password) // Sub Tests