Skip to content

Commit 647dacc

Browse files
tsenartAntonioMA
andcommitted
lib,cmd: add -connect-to flag
Closes #692, #691, #575 Co-authored-by: [email protected] Co-authored-by: Antonio M. Amaya <[email protected]>
1 parent ce93cd8 commit 647dacc

File tree

5 files changed

+165
-1
lines changed

5 files changed

+165
-1
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ attack command:
9292
TLS client PEM encoded certificate file
9393
-chunked
9494
Send body with chunked transfer encoding
95+
-connect-to value
96+
A mapping of (ip|host):port to use instead of a target URL's (ip|host):port. Can be repeated multiple times.
97+
Identical src:port with different dst:port will round-robin over the different dst:port pairs.
98+
Example: google.com:80:localhost:6060
9599
-connections int
96100
Max open idle connections per target host (default 10000)
97101
-dns-ttl value
@@ -178,7 +182,6 @@ examples:
178182
vegeta report -type=json results.bin > metrics.json
179183
cat results.bin | vegeta plot > plot.html
180184
cat results.bin | vegeta report -type="hist[0,100ms,200ms,300ms]"
181-
182185
```
183186

184187
#### `-cpus`

attack.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func attackCmd() command {
6262
fs.StringVar(&opts.promAddr, "prometheus-addr", "", "Prometheus exporter listen address [empty = disabled]. Example: 0.0.0.0:8880")
6363
fs.Var(&dnsTTLFlag{&opts.dnsTTL}, "dns-ttl", "Cache DNS lookups for the given duration [-1 = disabled, 0 = forever]")
6464
fs.BoolVar(&opts.sessionTickets, "session-tickets", false, "Enable TLS session resumption using session tickets")
65+
fs.Var(&connectToFlag{&opts.connectTo}, "connect-to", "A mapping of (ip|host):port to use instead of a target URL's (ip|host):port. Can be repeated multiple times.\nIdentical src:port with different dst:port will round-robin over the different dst:port pairs.\nExample: google.com:80:localhost:6060")
6566
systemSpecificFlags(fs, opts)
6667

6768
return command{fs, func(args []string) error {
@@ -108,6 +109,7 @@ type attackOpts struct {
108109
promAddr string
109110
dnsTTL time.Duration
110111
sessionTickets bool
112+
connectTo map[string][]string
111113
}
112114

113115
// attack validates the attack arguments, sets up the
@@ -218,6 +220,7 @@ func attack(opts *attackOpts) (err error) {
218220
vegeta.ProxyHeader(proxyHdr),
219221
vegeta.ChunkedBody(opts.chunked),
220222
vegeta.DNSCaching(opts.dnsTTL),
223+
vegeta.ConnectTo(opts.connectTo),
221224
vegeta.SessionTickets(opts.sessionTickets),
222225
)
223226

flags.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"math"
77
"net"
88
"net/http"
9+
"sort"
910
"strconv"
1011
"strings"
1112
"time"
@@ -153,3 +154,54 @@ func (f *dnsTTLFlag) String() string {
153154
}
154155
return f.ttl.String()
155156
}
157+
158+
const connectToFormat = "src:port:dst:port"
159+
160+
type connectToFlag struct {
161+
addrMap *map[string][]string
162+
}
163+
164+
func (c *connectToFlag) String() string {
165+
if c.addrMap == nil {
166+
return ""
167+
}
168+
169+
addrMappings := make([]string, 0, len(*c.addrMap))
170+
for k, v := range *c.addrMap {
171+
addrMappings = append(addrMappings, k+":"+strings.Join(v, ","))
172+
}
173+
174+
sort.Strings(addrMappings)
175+
return strings.Join(addrMappings, ";")
176+
}
177+
178+
func (c *connectToFlag) Set(s string) error {
179+
if c.addrMap == nil {
180+
return nil
181+
}
182+
183+
if *c.addrMap == nil {
184+
*c.addrMap = make(map[string][]string)
185+
}
186+
187+
parts := strings.Split(s, ":")
188+
if len(parts) != 4 {
189+
return fmt.Errorf("invalid -connect-to %q, expected format: %s", s, connectToFormat)
190+
}
191+
srcAddr := parts[0] + ":" + parts[1]
192+
dstAddr := parts[2] + ":" + parts[3]
193+
194+
// Parse source address
195+
if _, _, err := net.SplitHostPort(srcAddr); err != nil {
196+
return fmt.Errorf("invalid source address expression [%s], expected address:port", srcAddr)
197+
}
198+
199+
// Parse destination address
200+
if _, _, err := net.SplitHostPort(dstAddr); err != nil {
201+
return fmt.Errorf("invalid destination address expression [%s], expected address:port", dstAddr)
202+
}
203+
204+
(*c.addrMap)[srcAddr] = append((*c.addrMap)[srcAddr], dstAddr)
205+
206+
return nil
207+
}

lib/attack.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Attacker struct {
2828
maxWorkers uint64
2929
maxBody int64
3030
redirects int
31+
seqmu sync.Mutex
32+
seq uint64
33+
began time.Time
3134
chunked bool
3235
}
3336

@@ -272,6 +275,45 @@ func ProxyHeader(h http.Header) func(*Attacker) {
272275
}
273276
}
274277

278+
// ConnectTo returns a functional option which makes the attacker use the
279+
// passed in map to translate target addr:port pairs. When used with DNSCaching,
280+
// it must be used after it.
281+
func ConnectTo(addrMap map[string][]string) func(*Attacker) {
282+
return func(a *Attacker) {
283+
if len(addrMap) == 0 {
284+
return
285+
}
286+
287+
tr, ok := a.client.Transport.(*http.Transport)
288+
if !ok {
289+
return
290+
}
291+
292+
dial := tr.DialContext
293+
if dial == nil {
294+
dial = a.dialer.DialContext
295+
}
296+
297+
type roundRobin struct {
298+
addrs []string
299+
n int
300+
}
301+
302+
connectTo := make(map[string]*roundRobin, len(addrMap))
303+
for k, v := range addrMap {
304+
connectTo[k] = &roundRobin{addrs: v}
305+
}
306+
307+
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
308+
if cm, ok := connectTo[addr]; ok {
309+
cm.n = (cm.n + 1) % len(cm.addrs)
310+
addr = cm.addrs[cm.n]
311+
}
312+
return dial(ctx, network, addr)
313+
}
314+
}
315+
}
316+
275317
// DNSCaching returns a functional option that enables DNS caching for
276318
// the given ttl. When ttl is zero cached entries will never expire.
277319
// When ttl is non-zero, this will start a refresh go-routine that updates

lib/attack_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"reflect"
1616
"strconv"
1717
"strings"
18+
"sync"
1819
"testing"
1920
"time"
2021

@@ -498,3 +499,66 @@ func TestFirstOfEachIPFamily(t *testing.T) {
498499
})
499500
}
500501
}
502+
503+
func TestAttackConnectTo(t *testing.T) {
504+
t.Parallel()
505+
var mu sync.Mutex
506+
hits := make(map[string]int)
507+
srvs := make(map[string]int)
508+
509+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
510+
mu.Lock()
511+
hits[r.Host]++
512+
mu.Unlock()
513+
})
514+
515+
addrs := make([]string, 3)
516+
for i := range addrs {
517+
ln, err := net.Listen("tcp", "127.0.0.1:0")
518+
if err != nil {
519+
t.Fatal(err)
520+
}
521+
addrs[i] = ln.Addr().String()
522+
523+
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
524+
mu.Lock()
525+
srvs[ln.Addr().String()]++
526+
mu.Unlock()
527+
handler.ServeHTTP(w, r)
528+
}))
529+
530+
srv.Listener = ln
531+
srv.Start()
532+
t.Cleanup(srv.Close)
533+
}
534+
535+
tr := NewStaticTargeter(
536+
Target{Method: "GET", URL: "http://sapo.pt:80"},
537+
Target{Method: "GET", URL: "http://sapo.pt:80"},
538+
Target{Method: "GET", URL: "http://sapo.pt:80"},
539+
Target{Method: "GET", URL: "http://" + addrs[0]},
540+
)
541+
542+
atk := NewAttacker(
543+
KeepAlive(false),
544+
ConnectTo(map[string][]string{"sapo.pt:80": addrs}),
545+
)
546+
547+
a := &attack{name: "TEST", began: time.Now()}
548+
for i := 0; i < 4; i++ {
549+
resp := atk.hit(tr, a)
550+
if resp.Error != "" {
551+
t.Fatal(resp.Error)
552+
}
553+
}
554+
555+
want := map[string]int{"sapo.pt:80": 3, addrs[0]: 1}
556+
if diff := cmp.Diff(want, hits); diff != "" {
557+
t.Errorf("unexpected hits (-want +got):\n%s", diff)
558+
}
559+
560+
want = map[string]int{addrs[0]: 2, addrs[1]: 1, addrs[2]: 1}
561+
if diff := cmp.Diff(want, srvs); diff != "" {
562+
t.Errorf("unexpected hits (-want +got):\n%s", diff)
563+
}
564+
}

0 commit comments

Comments
 (0)