From cde1b3163c6e1eac0a93eb350a6501aa5cd3f8c1 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Fri, 6 Jul 2018 13:09:45 -0700 Subject: [PATCH 01/29] Added the ability to pause a process for a period of time. --- .../cppforlife/turbulence/agent/agent.go | 3 + .../cppforlife/turbulence/tasks/options.go | 8 +++ .../turbulence/tasks/pause_process_task.go | 65 +++++++++++++++++++ .../tasks/{optional_timeout.go => timeout.go} | 14 +++- 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/github.com/cppforlife/turbulence/tasks/pause_process_task.go rename src/github.com/cppforlife/turbulence/tasks/{optional_timeout.go => timeout.go} (54%) diff --git a/src/github.com/cppforlife/turbulence/agent/agent.go b/src/github.com/cppforlife/turbulence/agent/agent.go index cf204f0..1a944f3 100644 --- a/src/github.com/cppforlife/turbulence/agent/agent.go +++ b/src/github.com/cppforlife/turbulence/agent/agent.go @@ -146,6 +146,9 @@ func (a Agent) buildAgentTask(task tasks.Task) (agentTask, error) { t = tasks.NewKillProcessTask(monitClient, a.cmdRunner, opts, a.logger) } + case tasks.PauseProcessOptions: + t = tasks.NewPauseProcessTask(a.cmdRunner, opts, a.logger) + case tasks.StressOptions: t = tasks.NewStressTask(a.cmdRunner, opts, a.logger) diff --git a/src/github.com/cppforlife/turbulence/tasks/options.go b/src/github.com/cppforlife/turbulence/tasks/options.go index fc6e456..5f446e1 100644 --- a/src/github.com/cppforlife/turbulence/tasks/options.go +++ b/src/github.com/cppforlife/turbulence/tasks/options.go @@ -44,6 +44,10 @@ func (s *OptionsSlice) UnmarshalJSON(data []byte) error { var o KillProcessOptions err, opts = json.Unmarshal(bytes, &o), o + case optType == OptionsType(PauseProcessOptions{}): + var o PauseProcessOptions + err, opts = json.Unmarshal(bytes, &o), o + case optType == OptionsType(StressOptions{}): var o StressOptions err, opts = json.Unmarshal(bytes, &o), o @@ -96,6 +100,10 @@ func (s OptionsSlice) MarshalJSON() ([]byte, error) { typedO.Type = OptionsType(typedO) s[i] = typedO + case PauseProcessOptions: + typedO.Type = OptionsType(typedO) + s[i] = typedO + case StressOptions: typedO.Type = OptionsType(typedO) s[i] = typedO diff --git a/src/github.com/cppforlife/turbulence/tasks/pause_process_task.go b/src/github.com/cppforlife/turbulence/tasks/pause_process_task.go new file mode 100644 index 0000000..107f275 --- /dev/null +++ b/src/github.com/cppforlife/turbulence/tasks/pause_process_task.go @@ -0,0 +1,65 @@ +package tasks + +import( + bosherr "github.com/cloudfoundry/bosh-utils/errors" + boshlog "github.com/cloudfoundry/bosh-utils/logger" + boshsys "github.com/cloudfoundry/bosh-utils/system" +) + +type PauseProcessOptions struct { + Type string + + // Times may be suffixed with s,m,h,d,y + Timeout string + + // Process pattern used with pkill to select what processes are paused. + ProcessName string +} + +func (PauseProcessOptions) _private() {} + +type PauseProcessTask struct { + cmdRunner boshsys.CmdRunner + opts PauseProcessOptions + + logTag string + logger boshlog.Logger +} + +func NewPauseProcessTask( + cmdRunner boshsys.CmdRunner, + opts PauseProcessOptions, + logger boshlog.Logger, +) PauseProcessTask { + return PauseProcessTask{cmdRunner, opts, "tasks.PauseProcessTask", logger} +} + +func (t PauseProcessTask) Execute(stopCh chan struct{}) error { + timeoutCh, err := NewMandatoryTimeoutCh(t.opts.Timeout) + if err != nil { + return err + } + + t.logger.Debug(t.logTag, "Pausing processes matching '%s'", t.opts.ProcessName) + + _, _, exitStatus, err := t.cmdRunner.RunCommand("pkill", "-STOP", t.opts.ProcessName) + if err != nil { + return bosherr.WrapError(err, "Pausing process") + } else if exitStatus != 0 { + return bosherr.Errorf("pkill exited with status %d", exitStatus) + } + + select { + case <-timeoutCh: + case <-stopCh: + } + + _, _, exitStatus, err = t.cmdRunner.RunCommand("pkill", "-CONT", t.opts.ProcessName) + if err != nil { + return bosherr.WrapError(err, "Resuming process") + } else if exitStatus != 0 { + return bosherr.Errorf("pkill exited with status %d", exitStatus) + } + + return nil +} \ No newline at end of file diff --git a/src/github.com/cppforlife/turbulence/tasks/optional_timeout.go b/src/github.com/cppforlife/turbulence/tasks/timeout.go similarity index 54% rename from src/github.com/cppforlife/turbulence/tasks/optional_timeout.go rename to src/github.com/cppforlife/turbulence/tasks/timeout.go index 21c044d..a5d9848 100644 --- a/src/github.com/cppforlife/turbulence/tasks/optional_timeout.go +++ b/src/github.com/cppforlife/turbulence/tasks/timeout.go @@ -7,6 +7,18 @@ import ( ) func NewOptionalTimeoutCh(timeoutStr string) (<-chan time.Time, error) { + return newTimeoutCh(timeoutStr, true) +} + +func NewMandatoryTimeoutCh(timeoutStr string) (<- chan time.Time, error) { + return newTimeoutCh(timeoutStr, false) +} + +func newTimeoutCh(timeoutStr string, optional bool) (<-chan time.Time, error) { + if optional && len(timeoutStr) == 0 { + return nil, bosherr.Error("Timeout must be specified.") + } + if len(timeoutStr) == 0 { return make(chan time.Time), nil // never fires } @@ -17,4 +29,4 @@ func NewOptionalTimeoutCh(timeoutStr string) (<-chan time.Time, error) { } return time.After(timeout), nil -} +} \ No newline at end of file From bf9b170ef526a7fbce2d5d7b09d69580802739fd Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Fri, 6 Jul 2018 15:16:30 -0700 Subject: [PATCH 02/29] First attempt at v0.11.0 --- releases/turbulence/index.yml | 2 ++ releases/turbulence/turbulence-0.11.0.yml | 34 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 releases/turbulence/turbulence-0.11.0.yml diff --git a/releases/turbulence/index.yml b/releases/turbulence/index.yml index 841adb5..d24db6f 100644 --- a/releases/turbulence/index.yml +++ b/releases/turbulence/index.yml @@ -17,6 +17,8 @@ builds: version: "0.5" c747145a-e3c3-46de-68cd-04bed2cb800e: version: 0.10.0 + c747145a-e3c3-46de-68cd-04bed2cb800e: + version: 0.11.0 faf38e36-021c-43d2-40f4-6550d1578c47: version: "0.6" format-version: "2" diff --git a/releases/turbulence/turbulence-0.11.0.yml b/releases/turbulence/turbulence-0.11.0.yml new file mode 100644 index 0000000..568b3f7 --- /dev/null +++ b/releases/turbulence/turbulence-0.11.0.yml @@ -0,0 +1,34 @@ +name: turbulence +version: 0.11.0 +commit_hash: "cde1b31" +uncommitted_changes: false +jobs: +- name: turbulence_agent + version: 3f78d6827e535bda9749994db63132dfe81fa641 + fingerprint: 3f78d6827e535bda9749994db63132dfe81fa641 + sha1: 9413aedc8726c02b71fee4cb9f841b27a0a957b7 +- name: turbulence_api + version: ad7b0790bafad556d71f13ac6b4140fe158d8a65 + fingerprint: ad7b0790bafad556d71f13ac6b4140fe158d8a65 + sha1: 3fd84ee49750027bf2a8c762e37c2c2aa6189ddb +packages: +- name: golang-1.8-linux + version: abfa1e85ea18f3aa51930bf392e19bb692c55fcb1647c681b249e1ae7616de67 + fingerprint: abfa1e85ea18f3aa51930bf392e19bb692c55fcb1647c681b249e1ae7616de67 + sha1: sha256:ad9b3c801facf6d215ca956a69ed92d7cca71a3891e1a72a9e6876d0023d0851 + dependencies: [] +- name: stress + version: 6b00034151fd5be78893a537bd38818ad2a36bef + fingerprint: 6b00034151fd5be78893a537bd38818ad2a36bef + sha1: d50466dbf9d89858cbfc0885fa58f16c0b5a5320 + dependencies: [] +- name: turbulence + version: 65fa2f3a5ab4599ff4bc8ee8dad71ca4d90be6d5 + fingerprint: 65fa2f3a5ab4599ff4bc8ee8dad71ca4d90be6d5 + sha1: 4c3a1ed8ef3b46ef1626d44f59091190297d3e74 + dependencies: + - golang-1.8-linux +license: + version: 20f5c57713d88e62da7ca2d033365f09d096cfb2 + fingerprint: 20f5c57713d88e62da7ca2d033365f09d096cfb2 + sha1: a7f1f52c4488e5682311b832842a4e09dd2d45e7 From 8ef4ebbd59c20b3fc94d517e26cc10c1ecd51975 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 9 Jul 2018 13:36:54 -0700 Subject: [PATCH 03/29] Create .gitattributes Fix for Windows/Linux CRLF issues with shell scripts. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcadb2c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf From 054379a3339d814d7f3b9537633e81c3c3b0b89e Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 9 Jul 2018 13:39:17 -0700 Subject: [PATCH 04/29] Added file to link src to gopath for development purposes. --- src/link.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/link.py diff --git a/src/link.py b/src/link.py new file mode 100644 index 0000000..4a59afb --- /dev/null +++ b/src/link.py @@ -0,0 +1,13 @@ +import os + +PATHS=["github.com"] +GOPATH=os.environ["GOPATH"].split(':;')[0] + +for path in PATHS: + abs_path = os.path.abspath("./" + path) + for parent in os.scandir("./" + path): + if not parent.is_dir(): continue + os.makedirs("{}/src/{}/{}".format(GOPATH, path, parent.name), exist_ok=True) + for sub in os.scandir(parent.path): + if not sub.is_dir(): continue + os.symlink(os.path.abspath(sub.path), "{}/src/{}/{}/{}".format(GOPATH, path, parent.name, sub.name), target_is_directory=True) From 2fafae91c02175053bc15f8d3da6ff81f2947892 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 9 Jul 2018 13:45:43 -0700 Subject: [PATCH 05/29] Revert "First attempt at v0.11.0" This reverts commit bf9b170ef526a7fbce2d5d7b09d69580802739fd. --- releases/turbulence/index.yml | 2 -- releases/turbulence/turbulence-0.11.0.yml | 34 ----------------------- 2 files changed, 36 deletions(-) delete mode 100644 releases/turbulence/turbulence-0.11.0.yml diff --git a/releases/turbulence/index.yml b/releases/turbulence/index.yml index d24db6f..841adb5 100644 --- a/releases/turbulence/index.yml +++ b/releases/turbulence/index.yml @@ -17,8 +17,6 @@ builds: version: "0.5" c747145a-e3c3-46de-68cd-04bed2cb800e: version: 0.10.0 - c747145a-e3c3-46de-68cd-04bed2cb800e: - version: 0.11.0 faf38e36-021c-43d2-40f4-6550d1578c47: version: "0.6" format-version: "2" diff --git a/releases/turbulence/turbulence-0.11.0.yml b/releases/turbulence/turbulence-0.11.0.yml deleted file mode 100644 index 568b3f7..0000000 --- a/releases/turbulence/turbulence-0.11.0.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: turbulence -version: 0.11.0 -commit_hash: "cde1b31" -uncommitted_changes: false -jobs: -- name: turbulence_agent - version: 3f78d6827e535bda9749994db63132dfe81fa641 - fingerprint: 3f78d6827e535bda9749994db63132dfe81fa641 - sha1: 9413aedc8726c02b71fee4cb9f841b27a0a957b7 -- name: turbulence_api - version: ad7b0790bafad556d71f13ac6b4140fe158d8a65 - fingerprint: ad7b0790bafad556d71f13ac6b4140fe158d8a65 - sha1: 3fd84ee49750027bf2a8c762e37c2c2aa6189ddb -packages: -- name: golang-1.8-linux - version: abfa1e85ea18f3aa51930bf392e19bb692c55fcb1647c681b249e1ae7616de67 - fingerprint: abfa1e85ea18f3aa51930bf392e19bb692c55fcb1647c681b249e1ae7616de67 - sha1: sha256:ad9b3c801facf6d215ca956a69ed92d7cca71a3891e1a72a9e6876d0023d0851 - dependencies: [] -- name: stress - version: 6b00034151fd5be78893a537bd38818ad2a36bef - fingerprint: 6b00034151fd5be78893a537bd38818ad2a36bef - sha1: d50466dbf9d89858cbfc0885fa58f16c0b5a5320 - dependencies: [] -- name: turbulence - version: 65fa2f3a5ab4599ff4bc8ee8dad71ca4d90be6d5 - fingerprint: 65fa2f3a5ab4599ff4bc8ee8dad71ca4d90be6d5 - sha1: 4c3a1ed8ef3b46ef1626d44f59091190297d3e74 - dependencies: - - golang-1.8-linux -license: - version: 20f5c57713d88e62da7ca2d033365f09d096cfb2 - fingerprint: 20f5c57713d88e62da7ca2d033365f09d096cfb2 - sha1: a7f1f52c4488e5682311b832842a4e09dd2d45e7 From b8a6c7572d4bf4ab1c0a11a0a81098914c35aa58 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Tue, 10 Jul 2018 09:01:49 -0700 Subject: [PATCH 06/29] Updated docs to include PauseProcess. --- docs/api.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/api.md b/docs/api.md index 26d932b..7b4364c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -208,6 +208,25 @@ Example: } ``` +### Pause Process + +Pause one or more process on the VM associated with an instance. + +Configuration: + +- set `ProcessName`(string) to a pattern used with `pkill` +- set `timeout` (string) to how long the process should remain paused. A valid timeout is required. + +Example: + +```json +{ + "Type": "PauseProcess", + "ProcessName": "sshd" + "Timeout": "10m" // Times may be suffixed with s,m,h,d,y +} +``` + ### Stress Stresses different subsystems on the VM associated with an instance. From 51d13c7d497f4a73a202a79581a216ba4d64d8b7 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Tue, 10 Jul 2018 09:03:08 -0700 Subject: [PATCH 07/29] Update api.md --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 7b4364c..3c18081 100644 --- a/docs/api.md +++ b/docs/api.md @@ -222,7 +222,7 @@ Example: ```json { "Type": "PauseProcess", - "ProcessName": "sshd" + "ProcessName": "sshd", "Timeout": "10m" // Times may be suffixed with s,m,h,d,y } ``` From 43a14bd5042c1f4f1a02897c4178589610be99c6 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Thu, 12 Jul 2018 09:17:56 -0700 Subject: [PATCH 08/29] Escaped '*' --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 3c18081..7074a7f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -84,7 +84,7 @@ Available selector rules: - set `Values` (array of strings; optional) - set `Limit` (string; optional) -Limits default to 100%. Name defaults to '*' and wildcard matches are supported. +Limits default to 100%. Name defaults to '\*' and wildcard matches are supported. ```json { From 350cf1f0427064d95995c004344e48d08d2bc332 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Thu, 12 Jul 2018 09:19:31 -0700 Subject: [PATCH 09/29] Fixed issue #21 Fixed the error when using multiple params in control net task which was caused by repeated `tc qdist add ...` commands being run for the same network interface. --- .../turbulence/tasks/control_net_task.go | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index d660469..5dcaa9e 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -48,6 +48,8 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { return err } + opts := make([]string, 0, 16) + if len(t.opts.Delay) > 0 { variation := t.opts.DelayVariation @@ -55,12 +57,7 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { variation = "10ms" } - for _, ifaceName := range ifaceNames { - err := t.configureDelay(ifaceName, t.opts.Delay, variation) - if err != nil { - return err - } - } + opts = append(opts, "delay", t.opts.Delay, variation, "distribution", "normal") } if len(t.opts.Loss) > 0 { @@ -70,11 +67,13 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { correlation = "75%" } - for _, ifaceName := range ifaceNames { - err := t.configurePacketLoss(ifaceName, t.opts.Loss, correlation) - if err != nil { - return err - } + opts = append(opts, "loss", t.opts.Loss, correlation) + } + + for _, ifaceName := range ifaceNames { + err := t.configureInterface(ifaceName, opts) + if err != nil { + return err } } @@ -93,29 +92,13 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { return nil } -func (t ControlNetTask) configureDelay(ifaceName, delay, variation string) error { - args := []string{ - "qdisc", "add", "dev", ifaceName, "root", - "netem", "delay", delay, variation, "distribution", "normal", - } - - _, _, _, err := t.cmdRunner.RunCommand("tc", args...) - if err != nil { - return bosherr.WrapError(err, "Shelling out to tc to add delay") - } - - return nil -} - -func (t ControlNetTask) configurePacketLoss(ifaceName, percent, correlation string) error { - args := []string{ - "qdisc", "add", "dev", ifaceName, "root", - "netem", "loss", percent, correlation, - } +func (t ControlNetTask) configureInterface(ifaceName string, opts []string) error { + args := []string{"qdisc", "add", "dev", ifaceName, "root", "netem"} + args = append(args, opts...) _, _, _, err := t.cmdRunner.RunCommand("tc", args...) if err != nil { - return bosherr.WrapError(err, "Shelling out to tc to add packet loss") + return bosherr.WrapError(err, "Shelling out to tc netem") } return nil From ef1a723e80cd1fd714bd921b9241ab5e50fd7dac Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Thu, 12 Jul 2018 12:15:48 -0700 Subject: [PATCH 10/29] Added new functions to Control Network Added packet duplication, packet corruption, packet reordering, and bandwidth limiting. --- docs/api.md | 14 +++ .../turbulence/tasks/control_net_task.go | 114 ++++++++++++++---- 2 files changed, 102 insertions(+), 26 deletions(-) diff --git a/docs/api.md b/docs/api.md index 7074a7f..9014057 100644 --- a/docs/api.md +++ b/docs/api.md @@ -293,10 +293,24 @@ One or both of the following configurations must be selected: - packet delay - set `Delay` (string; required). Must be suffixed with `ms`. - set `DelayVariation` (string; optional). Must be suffixed with `ms`. Default is `10ms`. + - if `DelayVariation >= 0.5*Delay`, then packet reordering may occur. - packet loss - set `Loss` (string; required). Must be suffixed with `%`. - set `LossCorrelation` (string; optional). Must be suffixed with `%`. Default is `75%`. + +- packet duplication + - set `Duplication` (string; required). Must be suffixed with `%`. + +- packet corruption + - set `Corruption` (string; required). Must be suffixed with `%`. + +- packet reordering + - set `Reorder` (string; required). Must be suffixed with `%`. + - set `ReorderCorrelation` (string; optional). Must be suffixed with `%`. Default is `50%`. + - if the `Delay` is less than the inter-packet arrival time, then no reordering will be observed. + + Example: diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index 5dcaa9e..f3c22c2 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -7,6 +7,7 @@ import ( ) // See http://www.linuxfoundation.org/collaborate/workgroups/networking/netem +// See http://mark.koli.ch/slowdown-throttle-bandwidth-linux-network-interface type ControlNetOptions struct { Type string Timeout string // Times may be suffixed with ms,s,m,h @@ -19,6 +20,20 @@ type ControlNetOptions struct { Loss string LossCorrelation string + // tc qdisc add dev eth0 root netem duplicate 1% + Duplication string + + // tc qdisc add dev eth0 root netem corrupt 0.1% + Corruption string + + // tc qdisc add dec eth0 root netem reorder 25% 50% + Reorder string + ReorderCorrelation string + + // tc qdisc add dev eth0 root handle 1:0 [netem...] + // tc qdisc add dev eth0 parent 1:1 handle 10: tfb rate 256kbit buffer 1600 limit 3000 + Bandwidth string + // reset: tc qdisc del dev eth0 root } @@ -33,14 +48,28 @@ func NewControlNetTask(cmdRunner boshsys.CmdRunner, opts ControlNetOptions, _ bo return ControlNetTask{cmdRunner, opts} } +func defaultStr(v, d string) string { + if len(v) == 0 { + return d + } else { + return v + } +} + func (t ControlNetTask) Execute(stopCh chan struct{}) error { timeoutCh, err := NewOptionalTimeoutCh(t.opts.Timeout) if err != nil { return err } - if len(t.opts.Delay) == 0 && len(t.opts.Loss) == 0 { - return bosherr.Error("Must specify delay or loss") + delay, loss, duplication, corruption, reorder, bandwidth := len(t.opts.Delay) == 0, len(t.opts.Loss) == 0, len(t.opts.Duplication) == 0, len(t.opts.Corruption) == 0, len(t.opts.Reorder) == 0, len(t.opts.Bandwidth) == 0 + + if !(delay || loss || duplication || corruption || reorder || bandwidth) { + return bosherr.Error("Must specify an effect") + } + + if bandwidth && (delay || loss || duplication || corruption || reorder) { + return bosherr.Error("Cannot limit the bandwidth at the same time as other effects") } ifaceNames, err := NonLocalIfaceNames() @@ -48,32 +77,46 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { return err } - opts := make([]string, 0, 16) - - if len(t.opts.Delay) > 0 { - variation := t.opts.DelayVariation - - if len(variation) == 0 { - variation = "10ms" + if bandwidth { + for _, ifaceName := range ifaceNames { + err := t.configureBandwidth(ifaceName) + if err != nil { + t.resetIface(ifaceName) + return err + } } - - opts = append(opts, "delay", t.opts.Delay, variation, "distribution", "normal") - } - - if len(t.opts.Loss) > 0 { - correlation := t.opts.LossCorrelation - - if len(correlation) == 0 { - correlation = "75%" + } else { + opts := make([]string, 0, 16) + + if delay { + variation := defaultStr(t.opts.DelayVariation, "10ms") + opts = append(opts, "delay", t.opts.Delay, variation, "distribution", "normal") + } + + if loss { + correlation := defaultStr(t.opts.LossCorrelation, "75%") + opts = append(opts, "loss", t.opts.Loss, correlation) + } + + if duplication { + opts = append(opts, "duplicate", t.opts.Duplication) + } + + if corruption { + opts = append(opts, "corrupt", t.opts.Corruption) + } + + if reorder { + correlation := defaultStr(t.opts.ReorderCorrelation, "50%") + opts = append(opts, "reorder", t.opts.Reorder, correlation) } - opts = append(opts, "loss", t.opts.Loss, correlation) - } - - for _, ifaceName := range ifaceNames { - err := t.configureInterface(ifaceName, opts) - if err != nil { - return err + for _, ifaceName := range ifaceNames { + err := t.configureInterface(ifaceName, opts) + if err != nil { + t.resetIface(ifaceName) + return err + } } } @@ -98,7 +141,26 @@ func (t ControlNetTask) configureInterface(ifaceName string, opts []string) erro _, _, _, err := t.cmdRunner.RunCommand("tc", args...) if err != nil { - return bosherr.WrapError(err, "Shelling out to tc netem") + return bosherr.WrapError(err, "Shelling out to tc") + } + + return nil +} + +func (t ControlNetTask) configureBandwidth(ifaceName string) error { + _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "handle", "1:", "root", "htb", "default", "11") + if err != nil { + return err + } + + _, _, _, err = t.cmdRunner.RunCommand("tc", "class", "add", "dev", ifaceName, "parent", "1:", "classid", "1:1", "htb", "rate", t.opts.Bandwidth) + if err != nil { + return err + } + + _, _, _, err = t.cmdRunner.RunCommand("tc", "class", "add", "dev", ifaceName, "parent", "1:1", "classid", "1:11", "htb", "rate", t.opts.Bandwidth) + if err != nil { + return err } return nil From 6a83208dec0980a6fdc6f43c2be0d704deda6570 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Thu, 12 Jul 2018 12:57:41 -0700 Subject: [PATCH 11/29] Updated documentation --- docs/api.md | 4 +++- docs/dev.md | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 9014057..c8d6aac 100644 --- a/docs/api.md +++ b/docs/api.md @@ -310,7 +310,9 @@ One or both of the following configurations must be selected: - set `ReorderCorrelation` (string; optional). Must be suffixed with `%`. Default is `50%`. - if the `Delay` is less than the inter-packet arrival time, then no reordering will be observed. - +- bandwidth limiting + - set `Bandwidth` (string; required). Must be suffixed with one of `kbps`, `mbps` or `gbps`. + - bandwidth limiting must be used without any other effects. Example: diff --git a/docs/dev.md b/docs/dev.md index a5a1ce0..74a5942 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -15,8 +15,6 @@ Run `./update-deps` to update `github.com/cppforlife/turbulence` package depende - lock up whole machine - remount disk as readonly - corrupt disks -- pause a process -- restrict X% bandw https://www.kernel.org/doc/Documentation/sysrq.txt might be useful... http://blog.hut8labs.com/gorillas-before-monkeys.html From bdcdd879fac3698764b1e35e078836261062336e Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Thu, 12 Jul 2018 12:57:53 -0700 Subject: [PATCH 12/29] Fixed logic error in control network code --- .../turbulence/tasks/control_net_task.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index f3c22c2..adb872a 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -27,7 +27,7 @@ type ControlNetOptions struct { Corruption string // tc qdisc add dec eth0 root netem reorder 25% 50% - Reorder string + Reorder string ReorderCorrelation string // tc qdisc add dev eth0 root handle 1:0 [netem...] @@ -62,8 +62,8 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { return err } - delay, loss, duplication, corruption, reorder, bandwidth := len(t.opts.Delay) == 0, len(t.opts.Loss) == 0, len(t.opts.Duplication) == 0, len(t.opts.Corruption) == 0, len(t.opts.Reorder) == 0, len(t.opts.Bandwidth) == 0 - + delay, loss, duplication, corruption, reorder, bandwidth := len(t.opts.Delay) > 0, len(t.opts.Loss) > 0, len(t.opts.Duplication) > 0, len(t.opts.Corruption) > 0, len(t.opts.Reorder) > 0, len(t.opts.Bandwidth) > 0 + if !(delay || loss || duplication || corruption || reorder || bandwidth) { return bosherr.Error("Must specify an effect") } @@ -87,25 +87,25 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { } } else { opts := make([]string, 0, 16) - + if delay { variation := defaultStr(t.opts.DelayVariation, "10ms") opts = append(opts, "delay", t.opts.Delay, variation, "distribution", "normal") } - + if loss { correlation := defaultStr(t.opts.LossCorrelation, "75%") opts = append(opts, "loss", t.opts.Loss, correlation) } - + if duplication { opts = append(opts, "duplicate", t.opts.Duplication) } - + if corruption { opts = append(opts, "corrupt", t.opts.Corruption) } - + if reorder { correlation := defaultStr(t.opts.ReorderCorrelation, "50%") opts = append(opts, "reorder", t.opts.Reorder, correlation) From f6c3b1a15ee0ea330b2011e8739a348531a0cbc5 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Thu, 12 Jul 2018 13:25:52 -0700 Subject: [PATCH 13/29] Fixed issue where duplicate commands would cause others to be canceled out. --- .../cppforlife/turbulence/tasks/control_net_task.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index adb872a..abfc276 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -81,7 +81,6 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { for _, ifaceName := range ifaceNames { err := t.configureBandwidth(ifaceName) if err != nil { - t.resetIface(ifaceName) return err } } @@ -114,7 +113,6 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { for _, ifaceName := range ifaceNames { err := t.configureInterface(ifaceName, opts) if err != nil { - t.resetIface(ifaceName) return err } } @@ -155,11 +153,13 @@ func (t ControlNetTask) configureBandwidth(ifaceName string) error { _, _, _, err = t.cmdRunner.RunCommand("tc", "class", "add", "dev", ifaceName, "parent", "1:", "classid", "1:1", "htb", "rate", t.opts.Bandwidth) if err != nil { + t.resetIface(ifaceName) return err } _, _, _, err = t.cmdRunner.RunCommand("tc", "class", "add", "dev", ifaceName, "parent", "1:1", "classid", "1:11", "htb", "rate", t.opts.Bandwidth) if err != nil { + t.resetIface(ifaceName) return err } From de699fa1d8add8edd19aab9f82ef33d5b24e0ce7 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Thu, 12 Jul 2018 15:54:42 -0700 Subject: [PATCH 14/29] Updated fill disk to accept a timeout. --- .../turbulence/tasks/fill_disk_task.go | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go b/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go index 1a658d6..1894121 100644 --- a/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go @@ -8,6 +8,7 @@ import ( type FillDiskOptions struct { Type string + Timeout string // todo to percentage @@ -29,26 +30,53 @@ func NewFillDiskTask(cmdRunner boshsys.CmdRunner, opts FillDiskOptions, _ boshlo } func (t FillDiskTask) Execute(stopCh chan struct{}) error { + timeoutCh, err := NewOptionalTimeoutCh(t.opts.Timeout) + if err != nil { + return err + } + if t.opts.Persistent { - return t.fill("/var/vcap/store/.filler") + err = t.fill("/var/vcap/store/.filler") + } else if t.opts.Ephemeral { + err = t.fill("/var/vcap/data/.filler") + } else if t.opts.Temporary { + err = t.fill("/tmp/.filler") + } else { + err = t.fill("/.filler") + } + + if err != nil { + return err } - if t.opts.Ephemeral { - return t.fill("/var/vcap/data/.filler") + select { + case <-stopCh: + case <-timeoutCh: } - if t.opts.Temporary { - return t.fill("/tmp/.filler") + if t.opts.Persistent { + err = t.remove("/var/vcap/store/.filler") + } else if t.opts.Ephemeral { + err = t.remove("/var/vcap/data/.filler") + } else if t.opts.Temporary { + err = t.remove("/tmp/.filler") + } else { + err = t.remove("/.filler") } - return t.fill("/.filler") + return err } func (t FillDiskTask) fill(path string) error { _, _, _, err := t.cmdRunner.RunCommand("dd", "if=/dev/zero", "of="+path, "bs=1M") - if err != nil { + if err != nil && err.Error() != "dd: error writing ‘" + path + "’: No space left on device" { return bosherr.WrapError(err, "Filling disk") } return nil } + +func (t FillDiskTask) remove(path string) error { + _, _, _, err := t.cmdRunner.RunCommand("rm", path) + return err +} From 4b52b03c822561b94a8b9bd67a521ff4c718e10d Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Fri, 13 Jul 2018 14:55:18 -0700 Subject: [PATCH 15/29] Updates --- docs/api.md | 2 ++ .../turbulence/tasks/fill_disk_task.go | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/api.md b/docs/api.md index c8d6aac..964bc9a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -335,12 +335,14 @@ One of the following configurations must be selected: - set `Ephemeral` (bool) to fill up /var/vcap/data - set `Temporary` (bool) to fill up /tmp - by default uses root disk +- if multiple are selected, the first one in the above order will be used. Example: ```json { "Type": "FillDisk", + "Timeout": "10m", // Times may be suffixed with ms,s,m,h "Persistent": true } ``` diff --git a/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go b/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go index 1894121..da6f309 100644 --- a/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/fill_disk_task.go @@ -1,7 +1,7 @@ package tasks import ( - bosherr "github.com/cloudfoundry/bosh-utils/errors" + // bosherr "github.com/cloudfoundry/bosh-utils/errors" boshlog "github.com/cloudfoundry/bosh-utils/logger" boshsys "github.com/cloudfoundry/bosh-utils/system" ) @@ -23,10 +23,13 @@ func (FillDiskOptions) _private() {} type FillDiskTask struct { cmdRunner boshsys.CmdRunner opts FillDiskOptions + + logTag string + logger boshlog.Logger } -func NewFillDiskTask(cmdRunner boshsys.CmdRunner, opts FillDiskOptions, _ boshlog.Logger) FillDiskTask { - return FillDiskTask{cmdRunner, opts} +func NewFillDiskTask(cmdRunner boshsys.CmdRunner, opts FillDiskOptions, logger boshlog.Logger) FillDiskTask { + return FillDiskTask{cmdRunner, opts, "tasks.FillDiskTask", logger} } func (t FillDiskTask) Execute(stopCh chan struct{}) error { @@ -69,10 +72,11 @@ func (t FillDiskTask) Execute(stopCh chan struct{}) error { func (t FillDiskTask) fill(path string) error { _, _, _, err := t.cmdRunner.RunCommand("dd", "if=/dev/zero", "of="+path, "bs=1M") - if err != nil && err.Error() != "dd: error writing ‘" + path + "’: No space left on device" { - return bosherr.WrapError(err, "Filling disk") + if err != nil { + t.logger.Debug(t.logTag, "Encountered error filling disk: ", err) } - + // don't stop because of an error because it is probably from it running out of disk space which is to be expected + return nil } From 0d45fcf521bf0cda508af1df09993ca7e7083357 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 16 Jul 2018 10:46:03 -0700 Subject: [PATCH 16/29] Added BlockDNSTask --- .../cppforlife/turbulence/agent/agent.go | 3 + .../turbulence/tasks/block_dns_task.go | 74 +++++++++++++++++++ .../cppforlife/turbulence/tasks/options.go | 4 + 3 files changed, 81 insertions(+) create mode 100644 src/github.com/cppforlife/turbulence/tasks/block_dns_task.go diff --git a/src/github.com/cppforlife/turbulence/agent/agent.go b/src/github.com/cppforlife/turbulence/agent/agent.go index 1a944f3..c35aad4 100644 --- a/src/github.com/cppforlife/turbulence/agent/agent.go +++ b/src/github.com/cppforlife/turbulence/agent/agent.go @@ -158,6 +158,9 @@ func (a Agent) buildAgentTask(task tasks.Task) (agentTask, error) { case tasks.FirewallOptions: t = tasks.NewFirewallTask(a.cmdRunner, opts, a.agentConfig.AllowedOutputDests(), a.logger) + case tasks.BlockDNSOptions: + t = tasks.NewBlockDNSTask(a.cmdRunner, opts, a.logger) + case tasks.FillDiskOptions: t = tasks.NewFillDiskTask(a.cmdRunner, opts, a.logger) diff --git a/src/github.com/cppforlife/turbulence/tasks/block_dns_task.go b/src/github.com/cppforlife/turbulence/tasks/block_dns_task.go new file mode 100644 index 0000000..8b930e2 --- /dev/null +++ b/src/github.com/cppforlife/turbulence/tasks/block_dns_task.go @@ -0,0 +1,74 @@ +package tasks + +import ( + "strings" + + bosherr "github.com/cloudfoundry/bosh-utils/errors" + boshlog "github.com/cloudfoundry/bosh-utils/logger" + boshsys "github.com/cloudfoundry/bosh-utils/system" +) + +type BlockDNSOptions struct { + Type string + Timeout string // Times may be suffixed with ms,s,m,h +} + +func (BlockDNSOptions) _private() {} + +type BlockDNSTask struct { + cmdRunner boshsys.CmdRunner + opts BlockDNSOptions +} + +func NewBlockDNSTask( + cmdRunner boshsys.CmdRunner, + opts BlockDNSOptions, + _ boshlog.Logger, +) BlockDNSTask { + return BlockDNSTask{cmdRunner, opts} +} + +func (t BlockDNSTask) Execute(stopCh chan struct{}) error { + timeoutCh, err := NewOptionalTimeoutCh(t.opts.Timeout) + if err != nil { + return err + } + + rules := t.rules() + + for _, r := range rules { + err := t.iptables("-A", r) + if err != nil { + return err + } + } + + select { + case <-timeoutCh: + case <-stopCh: + } + + for _, r := range rules { + err := t.iptables("-D", r) + if err != nil { + return err + } + } + + return nil +} + +func (t BlockDNSTask) rules() []string { + return []string{ "OUTPUT -p tcp --destination-port 53 -j DROP", "OUTPUT -p udp --destination-port 53 -j DROP" } +} + +func (t BlockDNSTask) iptables(action, rule string) error { + args := append([]string{action}, strings.Split(rule, " ")...) + + _, _, _, err := t.cmdRunner.RunCommand("iptables", args...) + if err != nil { + return bosherr.WrapError(err, "Shelling out to iptables") + } + + return nil +} diff --git a/src/github.com/cppforlife/turbulence/tasks/options.go b/src/github.com/cppforlife/turbulence/tasks/options.go index 5f446e1..3379664 100644 --- a/src/github.com/cppforlife/turbulence/tasks/options.go +++ b/src/github.com/cppforlife/turbulence/tasks/options.go @@ -116,6 +116,10 @@ func (s OptionsSlice) MarshalJSON() ([]byte, error) { typedO.Type = OptionsType(typedO) s[i] = typedO + case BlockDNSOptions: + typedO.Type = OptionsType(typedO) + s[i] = typedO + case FillDiskOptions: typedO.Type = OptionsType(typedO) s[i] = typedO From 4d148d98c0d1b38170fe6bc07bbddda14a634918 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 16 Jul 2018 10:51:49 -0700 Subject: [PATCH 17/29] Updated docs and minor fix --- docs/api.md | 15 +++++++++++++++ .../cppforlife/turbulence/tasks/options.go | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/docs/api.md b/docs/api.md index 964bc9a..ac19044 100644 --- a/docs/api.md +++ b/docs/api.md @@ -282,6 +282,21 @@ Example: } ``` +### Block DNS + +Causes all outgoing DNS packets to be dropped. + +Currently iptables is used for dropping packets going out on tcp or udp port 53. + +Example: + +```json +{ + "Type": "BlockDNS", + "Timeout": "10m" // Times may be suffixed with ms,s,m,h +} +``` + ### Control Network Controls network quality on the VM associated with an instance. Does not affect `lo0`. diff --git a/src/github.com/cppforlife/turbulence/tasks/options.go b/src/github.com/cppforlife/turbulence/tasks/options.go index 3379664..0d53a8c 100644 --- a/src/github.com/cppforlife/turbulence/tasks/options.go +++ b/src/github.com/cppforlife/turbulence/tasks/options.go @@ -60,6 +60,10 @@ func (s *OptionsSlice) UnmarshalJSON(data []byte) error { var o FirewallOptions err, opts = json.Unmarshal(bytes, &o), o + case optType == OptionsType(BlockDNSOptions{}): + var o BlockDNSOptions + err, opts = json.Unmarshal(bytes, &o), o + case optType == OptionsType(FillDiskOptions{}): var o FillDiskOptions err, opts = json.Unmarshal(bytes, &o), o From 9e8c4f10f1106a622669ec42367a4d4545bf7fe0 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Fri, 27 Jul 2018 08:06:51 -0700 Subject: [PATCH 18/29] Added blackhole task for testing --- .../cppforlife/turbulence/agent/agent.go | 3 + .../turbulence/tasks/blackhole_task.go | 238 ++++++++++++++++++ .../cppforlife/turbulence/tasks/options.go | 8 + 3 files changed, 249 insertions(+) create mode 100644 src/github.com/cppforlife/turbulence/tasks/blackhole_task.go diff --git a/src/github.com/cppforlife/turbulence/agent/agent.go b/src/github.com/cppforlife/turbulence/agent/agent.go index c35aad4..2803904 100644 --- a/src/github.com/cppforlife/turbulence/agent/agent.go +++ b/src/github.com/cppforlife/turbulence/agent/agent.go @@ -158,6 +158,9 @@ func (a Agent) buildAgentTask(task tasks.Task) (agentTask, error) { case tasks.FirewallOptions: t = tasks.NewFirewallTask(a.cmdRunner, opts, a.agentConfig.AllowedOutputDests(), a.logger) + case tasks.BlackholeOptions: + t = tasks.NewBlackholeTask(a.cmdRunner, opts, a.logger) + case tasks.BlockDNSOptions: t = tasks.NewBlockDNSTask(a.cmdRunner, opts, a.logger) diff --git a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go new file mode 100644 index 0000000..a2898ee --- /dev/null +++ b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go @@ -0,0 +1,238 @@ +// See https://wiki.centos.org/HowTos/Network/IPTables for a good iptables tutorial + +package tasks + +import ( + "regexp" + "strings" + + bosherr "github.com/cloudfoundry/bosh-utils/errors" + boshlog "github.com/cloudfoundry/bosh-utils/logger" + boshsys "github.com/cloudfoundry/bosh-utils/system" +) + +type BlackholeOptions struct { + Type string + Timeout string // Times may be suffixed with ms,s,m,h + + Targets []BlackholeTarget +} + +var ipPattern = regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(/\d{0,2})?`) +var portPattern = regexp.MustCompile(`\d+(:\d+)?$`) + +// BlackholeTarget defines a rule for iptables. Each rule must contain one of {Host, DstPorts, SrcPorts}. +// If DstPorts or SrcPorts ports are included without a Host, then those ports will be blocked for all hosts. +// If Host is included without DstPorts or SrcPorts, then all traffic to/from those hosts will be blocked. +type BlackholeTarget struct { + // Optional host to block, can specify an address such as "10.34.4.60", an address block such as "192.168.0.0/24", + // or a domain name such as "google.com" which will be resolved to an Ip. + Host string + + // Optional direction to block traffic, must be in the set {INPUT, OUTPUT, BOTH}. Defaults to "BOTH". + Direction string + + // Optional protocol to block, must be in the set {udp, tcp, icmp, all}. Defaults to "all". + Protocol string + + // Optional "dport" or destination port(s) to block. Specify a single port such as "8080" or a range such as "4530:6740". + DstPorts string + + // Optional "sport" or source port(s) to block. Specify a single port such as "8080" or a range such as "4530:6740". + SrcPorts string +} + +func (BlackholeOptions) _private() {} + +type BlackholeTask struct { + cmdRunner boshsys.CmdRunner + opts BlackholeOptions + logger boshlog.Logger +} + +func NewBlackholeTask( + cmdRunner boshsys.CmdRunner, + opts BlackholeOptions, + logger boshlog.Logger, +) BlackholeTask { + return BlackholeTask{cmdRunner, opts, logger} +} + +func (t BlackholeTask) Execute(stopCh chan struct{}) error { + timeoutCh, err := NewOptionalTimeoutCh(t.opts.Timeout) + if err != nil { + return err + } + + rules, err := t.rules() + if err != nil { + return err + } + + for _, r := range rules { + err := t.iptables("-A", r) + if err != nil { + return err + } + } + + select { + case <-timeoutCh: + case <-stopCh: + } + + for _, r := range rules { + err := t.iptables("-D", r) + if err != nil { + return err + } + } + + return nil +} + +func (t BlackholeTask) rules() ([]string, error) { + var rules []string + for _, target := range t.opts.Targets { + if target.Host == "" && target.DstPorts == "" && target.SrcPorts == "" { + return nil, bosherr.Error("Must specify at least one of Host, DstPorts, and or SrcPorts.") + } + + var hosts []string + var direction, protocol, dports, sports string + + if target.Host == "" { + hosts = nil + } else if ipPattern.MatchString(target.Host) { + hosts = ipPattern.FindAllString(target.Host, -1) + } else { + var err error + hosts, err = t.dig(target.Host) + + if err != nil { + return nil, err + } + } + + switch strings.ToUpper(target.Direction) { + case "": + direction = "" + case "INPUT": + direction = "INPUT" + case "OUTPUT": + direction = "OUTPUT" + case "BOTH": + direction = "" + default: + return nil, bosherr.Errorf("Invalid direction '%v', must be one of {INPUT, OUTPUT, BOTH} or blank.", target.Direction) + } + + switch strings.ToLower(target.Direction) { + case "": + protocol = "all" + case "tcp": + protocol = "tcp" + case "udp": + protocol = "udp" + case "icmp": + protocol = "icmp" + case "all": + protocol = "all" + default: + return nil, bosherr.Errorf("Invalid protocol '%v', must be one of {tcp, udp, icmp, all} or blank.", target.Direction) + } + + if target.DstPorts == "" { + dports = "" + } else if portPattern.MatchString(target.DstPorts) { + dports = target.DstPorts + } else { + return nil, bosherr.Errorf("Invalid destination port specified %v", target.DstPorts) + } + + if target.SrcPorts == "" { + sports = "" + } else if portPattern.MatchString(target.SrcPorts) { + sports = target.SrcPorts + } else { + return nil, bosherr.Errorf("Invalid destination port specified %v", target.SrcPorts) + } + + + if direction == "" || direction == "INPUT" { + command := "INPUT" + + if hosts != nil { + command += " -s " + for i, ip := range hosts { + if i > 0 { command += ","} + command += ip + } + } + + command += " -p " + protocol + + if dports != "" { + command += " -dport " + dports + } + + if sports != "" { + command += " -sport " + sports + } + + rules = append(rules, command) + } + + if direction == "" || direction == "OUTPUT" { + command := "OUTPUT" + + if hosts != nil { + command += " -d " + for i, ip := range hosts { + if i > 0 { command += ","} + command += ip + } + } + + command += " -p " + protocol + + if dports != "" { + command += " -dport " + dports + } + + if sports != "" { + command += " -sport " + sports + } + + rules = append(rules, command) + } + } + + return rules, nil +} + +func (t BlackholeTask) dig(hostname string) ([]string, error) { + args := []string{"+short", hostname} + output, _, _, err := t.cmdRunner.RunCommand("dig", args...) + if err != nil { + return nil, bosherr.WrapError(err, "resolving host name") + } + + ips := ipPattern.FindAllString(output, -1) + if ips == nil { + return nil, bosherr.Errorf("No IPs found for host %v", hostname) + } + + return ips, nil +} + +func (t BlackholeTask) iptables(action, rule string) error { + args := append([]string{action}, strings.Split(rule, " ")...) + + _, _, _, err := t.cmdRunner.RunCommand("iptables", args...) + if err != nil { + return bosherr.WrapError(err, "Shelling out to iptables") + } + + return nil +} diff --git a/src/github.com/cppforlife/turbulence/tasks/options.go b/src/github.com/cppforlife/turbulence/tasks/options.go index 0d53a8c..cdeebdc 100644 --- a/src/github.com/cppforlife/turbulence/tasks/options.go +++ b/src/github.com/cppforlife/turbulence/tasks/options.go @@ -60,6 +60,10 @@ func (s *OptionsSlice) UnmarshalJSON(data []byte) error { var o FirewallOptions err, opts = json.Unmarshal(bytes, &o), o + case optType == OptionsType(BlackholeOptions{}): + var o BlackholeOptions + err, opts = json.Unmarshal(bytes, &o), o + case optType == OptionsType(BlockDNSOptions{}): var o BlockDNSOptions err, opts = json.Unmarshal(bytes, &o), o @@ -120,6 +124,10 @@ func (s OptionsSlice) MarshalJSON() ([]byte, error) { typedO.Type = OptionsType(typedO) s[i] = typedO + case BlackholeOptions: + typedO.Type = OptionsType(typedO) + s[i] = typedO + case BlockDNSOptions: typedO.Type = OptionsType(typedO) s[i] = typedO From e7e362c2bb2fa35c454d1f0541e7b3d0b815f4d1 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Fri, 27 Jul 2018 09:08:46 -0700 Subject: [PATCH 19/29] Fixed issue with blackhole iptables call --- src/github.com/cppforlife/turbulence/tasks/blackhole_task.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go index a2898ee..5dedcd6 100644 --- a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go @@ -180,6 +180,7 @@ func (t BlackholeTask) rules() ([]string, error) { command += " -sport " + sports } + command += " -j DROP" rules = append(rules, command) } @@ -204,6 +205,7 @@ func (t BlackholeTask) rules() ([]string, error) { command += " -sport " + sports } + command += " -j DROP" rules = append(rules, command) } } From 8aea83074c0c3b8b5075302d325b82d6681a35c8 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Fri, 27 Jul 2018 09:44:14 -0700 Subject: [PATCH 20/29] Updated docs --- docs/api.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/api.md b/docs/api.md index ac19044..939f38e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -282,6 +282,42 @@ Example: } ``` +### Blackhole + +Drops incoming and or outgoing traffic from one or more VMs. It is able to target specific IPs and Ports to simulate the failure of specific services. + +Currently iptables is used for dropping packets from INPUT and OUTPUT chains. + +Target parameters: + +- set `Host` (string) to either an IPv4 address such as "192.168.1.50" or with a mask such as "192.168.0.0/24", or to a domain name which will be resolved into (possibly multiple) IPs such as "example.com" using the dig command. If no host is specified, then all hosts will be impacted. +- set `Direction` (string) to the direction of traffic to drop, can be either "INPUT", "OUTPUT", or "BOTH". Defaults to "BOTH". +- set `Protocol` (string) to the protocol to drop traffic on, can be either "udp", "tcp", "icmp", or "all". Defaults to "all". +- set `DstPorts` (string) to the destination port to drop. This can be either a single port such as "8080" or a range such as "1503:1520". If blank, all destination ports will be dropped. +- set `SrcPorts` (string) to the source ports to drop. This can be either a single port such as "8080" or a range such as "1503:1520". If blank, all source ports will be dropped. + +*Note*: at least one of `Host`, `DstPorts`, and or `SrcPorts` must be specified. + +Example: + +```json +{ + "Type": "Blackhole", + "Timeout": "10m", // Times may be suffixed with ms,s,m,h + "Targets": [{ + "Host": "1.1.1.1", + "Direction": "INPUT", + "DstPorts": "53" + },{ + "Host": "google.com", + "Direction": "BOTH", + "Protocol": "tcp", + "DstPorts": "80" + }] +} +``` + + ### Block DNS Causes all outgoing DNS packets to be dropped. From 84829adf71c7e007034ab105168455a3b17eb3d2 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Fri, 27 Jul 2018 09:44:21 -0700 Subject: [PATCH 21/29] Fixed stupid error. --- src/github.com/cppforlife/turbulence/tasks/blackhole_task.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go index 5dedcd6..28067f0 100644 --- a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go @@ -127,7 +127,7 @@ func (t BlackholeTask) rules() ([]string, error) { return nil, bosherr.Errorf("Invalid direction '%v', must be one of {INPUT, OUTPUT, BOTH} or blank.", target.Direction) } - switch strings.ToLower(target.Direction) { + switch strings.ToLower(target.Protocol) { case "": protocol = "all" case "tcp": @@ -139,7 +139,7 @@ func (t BlackholeTask) rules() ([]string, error) { case "all": protocol = "all" default: - return nil, bosherr.Errorf("Invalid protocol '%v', must be one of {tcp, udp, icmp, all} or blank.", target.Direction) + return nil, bosherr.Errorf("Invalid protocol '%v', must be one of {tcp, udp, icmp, all} or blank.", target.Protocol) } if target.DstPorts == "" { From 83b8a012ee6cddc8586eb3904111701d8fc3959b Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 13 Aug 2018 12:43:59 -0700 Subject: [PATCH 22/29] Updates to blackhole and docs. --- docs/api.md | 6 +- .../turbulence/tasks/blackhole_task.go | 88 +++++++------------ 2 files changed, 36 insertions(+), 58 deletions(-) diff --git a/docs/api.md b/docs/api.md index 939f38e..0775ade 100644 --- a/docs/api.md +++ b/docs/api.md @@ -290,9 +290,9 @@ Currently iptables is used for dropping packets from INPUT and OUTPUT chains. Target parameters: +- set `Direction` (string; required) to the direction of traffic to drop, can be either "INPUT", "OUTPUT", or "FORWARD". If you are targeting diego-cells, then you will probably want "FORWARD". - set `Host` (string) to either an IPv4 address such as "192.168.1.50" or with a mask such as "192.168.0.0/24", or to a domain name which will be resolved into (possibly multiple) IPs such as "example.com" using the dig command. If no host is specified, then all hosts will be impacted. -- set `Direction` (string) to the direction of traffic to drop, can be either "INPUT", "OUTPUT", or "BOTH". Defaults to "BOTH". -- set `Protocol` (string) to the protocol to drop traffic on, can be either "udp", "tcp", "icmp", or "all". Defaults to "all". +- set `Protocol` (string) to the protocol to drop traffic on, can be either "udp", "tcp", "icmp", or "all". Defaults to being unspecified. - set `DstPorts` (string) to the destination port to drop. This can be either a single port such as "8080" or a range such as "1503:1520". If blank, all destination ports will be dropped. - set `SrcPorts` (string) to the source ports to drop. This can be either a single port such as "8080" or a range such as "1503:1520". If blank, all source ports will be dropped. @@ -310,7 +310,7 @@ Example: "DstPorts": "53" },{ "Host": "google.com", - "Direction": "BOTH", + "Direction": "FORWARD", "Protocol": "tcp", "DstPorts": "80" }] diff --git a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go index 28067f0..7d9639b 100644 --- a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go @@ -69,8 +69,10 @@ func (t BlackholeTask) Execute(stopCh chan struct{}) error { return err } - for _, r := range rules { - err := t.iptables("-A", r) + for _, rule := range rules { + r := []string{rule[0], "1"} // we want it inserted at the beginning of the rules or it may have no effect. + r = append(r, rule[1:]...) + err := t.iptables("-I", r) if err != nil { return err } @@ -91,8 +93,9 @@ func (t BlackholeTask) Execute(stopCh chan struct{}) error { return nil } -func (t BlackholeTask) rules() ([]string, error) { - var rules []string +func (t BlackholeTask) rules() ([][]string, error) { + rules := [][]string{} + for _, target := range t.opts.Targets { if target.Host == "" && target.DstPorts == "" && target.SrcPorts == "" { return nil, bosherr.Error("Must specify at least one of Host, DstPorts, and or SrcPorts.") @@ -115,21 +118,19 @@ func (t BlackholeTask) rules() ([]string, error) { } switch strings.ToUpper(target.Direction) { - case "": - direction = "" case "INPUT": direction = "INPUT" case "OUTPUT": direction = "OUTPUT" - case "BOTH": - direction = "" + case "FORWARD": + direction = "FORWARD" default: - return nil, bosherr.Errorf("Invalid direction '%v', must be one of {INPUT, OUTPUT, BOTH} or blank.", target.Direction) + return nil, bosherr.Errorf("Invalid direction '%v', must be one of {INPUT, OUTPUT, FORWARD}.", target.Direction) } switch strings.ToLower(target.Protocol) { case "": - protocol = "all" + protocol = "" case "tcp": protocol = "tcp" case "udp": @@ -158,56 +159,33 @@ func (t BlackholeTask) rules() ([]string, error) { return nil, bosherr.Errorf("Invalid destination port specified %v", target.SrcPorts) } + cmd := []string{direction} - if direction == "" || direction == "INPUT" { - command := "INPUT" - - if hosts != nil { - command += " -s " - for i, ip := range hosts { - if i > 0 { command += ","} - command += ip - } - } - - command += " -p " + protocol - - if dports != "" { - command += " -dport " + dports + if hosts != nil { + ips := "" + for i, ip := range hosts { + if i > 0 { ips += ","} + ips += ip } - if sports != "" { - command += " -sport " + sports - } - - command += " -j DROP" - rules = append(rules, command) + cmd = append(cmd, "-s", ips) } - if direction == "" || direction == "OUTPUT" { - command := "OUTPUT" - - if hosts != nil { - command += " -d " - for i, ip := range hosts { - if i > 0 { command += ","} - command += ip - } - } - - command += " -p " + protocol - - if dports != "" { - command += " -dport " + dports - } - - if sports != "" { - command += " -sport " + sports - } + if protocol != "" { + cmd = append(cmd, "-p", protocol) + } - command += " -j DROP" - rules = append(rules, command) + if dports != "" { + cmd = append(cmd, "--dport", dports) + } + + if sports != "" { + cmd = append(cmd, "--sport", sports) } + + cmd = append(cmd, "-j", "DROP") + + rules = append(rules, cmd) } return rules, nil @@ -228,8 +206,8 @@ func (t BlackholeTask) dig(hostname string) ([]string, error) { return ips, nil } -func (t BlackholeTask) iptables(action, rule string) error { - args := append([]string{action}, strings.Split(rule, " ")...) +func (t BlackholeTask) iptables(action string, rule []string) error { + args := append([]string{action}, rule...) _, _, _, err := t.cmdRunner.RunCommand("iptables", args...) if err != nil { From 1951ccfb11ec34a6c43860aca5c39580f35b8fa2 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 13 Aug 2018 14:25:51 -0700 Subject: [PATCH 23/29] Updated blackhole to make a distinction between src and dst hosts. --- docs/api.md | 9 +-- .../turbulence/tasks/blackhole_task.go | 69 ++++++++++++------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/api.md b/docs/api.md index 0775ade..14808f3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -291,12 +291,13 @@ Currently iptables is used for dropping packets from INPUT and OUTPUT chains. Target parameters: - set `Direction` (string; required) to the direction of traffic to drop, can be either "INPUT", "OUTPUT", or "FORWARD". If you are targeting diego-cells, then you will probably want "FORWARD". -- set `Host` (string) to either an IPv4 address such as "192.168.1.50" or with a mask such as "192.168.0.0/24", or to a domain name which will be resolved into (possibly multiple) IPs such as "example.com" using the dig command. If no host is specified, then all hosts will be impacted. +- set `SrcHost` (string) to either an IPv4 address such as "192.168.1.50" or with a mask such as "192.168.0.0/24", or to a domain name which will be resolved into (possibly multiple) IPs such as "example.com" using the dig command. If no host is specified, then all source hosts will be impacted. +- set `DstHost` (string) to either an IPv4 address such as "192.168.1.50" or with a mask such as "192.168.0.0/24", or to a domain name which will be resolved into (possibly multiple) IPs such as "example.com" using the dig command. If no host is specified, then all destination hosts will be impacted. - set `Protocol` (string) to the protocol to drop traffic on, can be either "udp", "tcp", "icmp", or "all". Defaults to being unspecified. - set `DstPorts` (string) to the destination port to drop. This can be either a single port such as "8080" or a range such as "1503:1520". If blank, all destination ports will be dropped. - set `SrcPorts` (string) to the source ports to drop. This can be either a single port such as "8080" or a range such as "1503:1520". If blank, all source ports will be dropped. -*Note*: at least one of `Host`, `DstPorts`, and or `SrcPorts` must be specified. +*Note*: at least one of `SrcHost`, `DstHost`, `DstPorts`, or `SrcPorts` must be specified. Example: @@ -305,11 +306,11 @@ Example: "Type": "Blackhole", "Timeout": "10m", // Times may be suffixed with ms,s,m,h "Targets": [{ - "Host": "1.1.1.1", + "DstHost": "1.1.1.1", "Direction": "INPUT", "DstPorts": "53" },{ - "Host": "google.com", + "DstHost": "google.com", "Direction": "FORWARD", "Protocol": "tcp", "DstPorts": "80" diff --git a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go index 7d9639b..5ef20e3 100644 --- a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go @@ -22,12 +22,16 @@ var ipPattern = regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(/\d{0,2 var portPattern = regexp.MustCompile(`\d+(:\d+)?$`) // BlackholeTarget defines a rule for iptables. Each rule must contain one of {Host, DstPorts, SrcPorts}. -// If DstPorts or SrcPorts ports are included without a Host, then those ports will be blocked for all hosts. +// If DstPorts or SrcPorts ports are included without a DstHost or SrcHost, then those ports will be blocked for all hosts. // If Host is included without DstPorts or SrcPorts, then all traffic to/from those hosts will be blocked. type BlackholeTarget struct { - // Optional host to block, can specify an address such as "10.34.4.60", an address block such as "192.168.0.0/24", + // Optional destination host to block, can specify an address such as "10.34.4.60", an address block such as "192.168.0.0/24", // or a domain name such as "google.com" which will be resolved to an Ip. - Host string + DstHost string + + // Optional source host to block, can specify an address such as "10.34.4.60", an address block such as "192.168.0.0/24", + // or a domain name such as "google.com" which will be resolved to an Ip. + SrcHost string // Optional direction to block traffic, must be in the set {INPUT, OUTPUT, BOTH}. Defaults to "BOTH". Direction string @@ -93,28 +97,45 @@ func (t BlackholeTask) Execute(stopCh chan struct{}) error { return nil } +func (t BlackholeTask) getHost(host string) ([]string, error) { + if host == "" { + return nil, nil + } else if ipPattern.MatchString(host) { + return ipPattern.FindAllString(host, -1), nil + } else { + return t.dig(host) + } +} + +func appendHosts(cmd []string, flag string, hosts ...string) []string { + ips := "" + for i, ip := range hosts { + if i > 0 { ips += ","} + ips += ip + } + + return append(cmd, "-d", ips) +} + func (t BlackholeTask) rules() ([][]string, error) { rules := [][]string{} for _, target := range t.opts.Targets { - if target.Host == "" && target.DstPorts == "" && target.SrcPorts == "" { - return nil, bosherr.Error("Must specify at least one of Host, DstPorts, and or SrcPorts.") + if target.SrcHost == "" && target.DstHost == "" && target.DstPorts == "" && target.SrcPorts == "" { + return nil, bosherr.Error("Must specify at least one of SrcHost, DstHost, DstPorts, and or SrcPorts.") } - var hosts []string + var dsthosts []string var direction, protocol, dports, sports string - if target.Host == "" { - hosts = nil - } else if ipPattern.MatchString(target.Host) { - hosts = ipPattern.FindAllString(target.Host, -1) - } else { - var err error - hosts, err = t.dig(target.Host) - - if err != nil { - return nil, err - } + srchosts, err := t.getHost(target.SrcHost) + if err != nil { + return nil, err + } + + dsthosts, err = t.getHost(target.DstHost) + if err != nil { + return nil, err } switch strings.ToUpper(target.Direction) { @@ -161,14 +182,12 @@ func (t BlackholeTask) rules() ([][]string, error) { cmd := []string{direction} - if hosts != nil { - ips := "" - for i, ip := range hosts { - if i > 0 { ips += ","} - ips += ip - } - - cmd = append(cmd, "-s", ips) + if dsthosts != nil { + cmd = appendHosts(cmd, "-d", dsthosts...) + } + + if srchosts != nil { + cmd = appendHosts(cmd, "-s", srchosts...) } if protocol != "" { From a4c0452588812f61129e88019c6f5954c31e20c8 Mon Sep 17 00:00:00 2001 From: Matthew Conover Date: Mon, 13 Aug 2018 15:40:15 -0700 Subject: [PATCH 24/29] Renamed Blackhole to TargetedBlocker --- docs/api.md | 4 +- .../cppforlife/turbulence/agent/agent.go | 4 +- .../cppforlife/turbulence/tasks/options.go | 6 +-- ...khole_task.go => targeted_blocker_task.go} | 54 ++++++++++--------- 4 files changed, 35 insertions(+), 33 deletions(-) rename src/github.com/cppforlife/turbulence/tasks/{blackhole_task.go => targeted_blocker_task.go} (84%) diff --git a/docs/api.md b/docs/api.md index 14808f3..0b51637 100644 --- a/docs/api.md +++ b/docs/api.md @@ -282,7 +282,7 @@ Example: } ``` -### Blackhole +### TargetedBlocker Drops incoming and or outgoing traffic from one or more VMs. It is able to target specific IPs and Ports to simulate the failure of specific services. @@ -303,7 +303,7 @@ Example: ```json { - "Type": "Blackhole", + "Type": "TargetedBlocker", "Timeout": "10m", // Times may be suffixed with ms,s,m,h "Targets": [{ "DstHost": "1.1.1.1", diff --git a/src/github.com/cppforlife/turbulence/agent/agent.go b/src/github.com/cppforlife/turbulence/agent/agent.go index 2803904..75352a9 100644 --- a/src/github.com/cppforlife/turbulence/agent/agent.go +++ b/src/github.com/cppforlife/turbulence/agent/agent.go @@ -158,8 +158,8 @@ func (a Agent) buildAgentTask(task tasks.Task) (agentTask, error) { case tasks.FirewallOptions: t = tasks.NewFirewallTask(a.cmdRunner, opts, a.agentConfig.AllowedOutputDests(), a.logger) - case tasks.BlackholeOptions: - t = tasks.NewBlackholeTask(a.cmdRunner, opts, a.logger) + case tasks.TargetedBlockerOptions: + t = tasks.NewTargetedBlockerTask(a.cmdRunner, opts, a.logger) case tasks.BlockDNSOptions: t = tasks.NewBlockDNSTask(a.cmdRunner, opts, a.logger) diff --git a/src/github.com/cppforlife/turbulence/tasks/options.go b/src/github.com/cppforlife/turbulence/tasks/options.go index cdeebdc..b8260af 100644 --- a/src/github.com/cppforlife/turbulence/tasks/options.go +++ b/src/github.com/cppforlife/turbulence/tasks/options.go @@ -60,8 +60,8 @@ func (s *OptionsSlice) UnmarshalJSON(data []byte) error { var o FirewallOptions err, opts = json.Unmarshal(bytes, &o), o - case optType == OptionsType(BlackholeOptions{}): - var o BlackholeOptions + case optType == OptionsType(TargetedBlockerOptions{}): + var o TargetedBlockerOptions err, opts = json.Unmarshal(bytes, &o), o case optType == OptionsType(BlockDNSOptions{}): @@ -124,7 +124,7 @@ func (s OptionsSlice) MarshalJSON() ([]byte, error) { typedO.Type = OptionsType(typedO) s[i] = typedO - case BlackholeOptions: + case TargetedBlockerOptions: typedO.Type = OptionsType(typedO) s[i] = typedO diff --git a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go b/src/github.com/cppforlife/turbulence/tasks/targeted_blocker_task.go similarity index 84% rename from src/github.com/cppforlife/turbulence/tasks/blackhole_task.go rename to src/github.com/cppforlife/turbulence/tasks/targeted_blocker_task.go index 5ef20e3..7664d0d 100644 --- a/src/github.com/cppforlife/turbulence/tasks/blackhole_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/targeted_blocker_task.go @@ -11,20 +11,20 @@ import ( boshsys "github.com/cloudfoundry/bosh-utils/system" ) -type BlackholeOptions struct { +type TargetedBlockerOptions struct { Type string Timeout string // Times may be suffixed with ms,s,m,h - Targets []BlackholeTarget + Targets []Target } var ipPattern = regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(/\d{0,2})?`) var portPattern = regexp.MustCompile(`\d+(:\d+)?$`) -// BlackholeTarget defines a rule for iptables. Each rule must contain one of {Host, DstPorts, SrcPorts}. +// Target defines a rule for iptables. Each rule must contain one of {Host, DstPorts, SrcPorts}. // If DstPorts or SrcPorts ports are included without a DstHost or SrcHost, then those ports will be blocked for all hosts. // If Host is included without DstPorts or SrcPorts, then all traffic to/from those hosts will be blocked. -type BlackholeTarget struct { +type Target struct { // Optional destination host to block, can specify an address such as "10.34.4.60", an address block such as "192.168.0.0/24", // or a domain name such as "google.com" which will be resolved to an Ip. DstHost string @@ -46,23 +46,23 @@ type BlackholeTarget struct { SrcPorts string } -func (BlackholeOptions) _private() {} +func (TargetedBlockerOptions) _private() {} -type BlackholeTask struct { +type TargetedBlockerTask struct { cmdRunner boshsys.CmdRunner - opts BlackholeOptions - logger boshlog.Logger + opts TargetedBlockerOptions + logger boshlog.Logger } -func NewBlackholeTask( +func NewTargetedBlockerTask( cmdRunner boshsys.CmdRunner, - opts BlackholeOptions, + opts TargetedBlockerOptions, logger boshlog.Logger, -) BlackholeTask { - return BlackholeTask{cmdRunner, opts, logger} +) TargetedBlockerTask { + return TargetedBlockerTask{cmdRunner, opts, logger} } -func (t BlackholeTask) Execute(stopCh chan struct{}) error { +func (t TargetedBlockerTask) Execute(stopCh chan struct{}) error { timeoutCh, err := NewOptionalTimeoutCh(t.opts.Timeout) if err != nil { return err @@ -74,7 +74,7 @@ func (t BlackholeTask) Execute(stopCh chan struct{}) error { } for _, rule := range rules { - r := []string{rule[0], "1"} // we want it inserted at the beginning of the rules or it may have no effect. + r := []string{rule[0], "1"} // we want it inserted at the beginning of the rules or it may have no effect. r = append(r, rule[1:]...) err := t.iptables("-I", r) if err != nil { @@ -97,7 +97,7 @@ func (t BlackholeTask) Execute(stopCh chan struct{}) error { return nil } -func (t BlackholeTask) getHost(host string) ([]string, error) { +func (t TargetedBlockerTask) getHost(host string) ([]string, error) { if host == "" { return nil, nil } else if ipPattern.MatchString(host) { @@ -110,14 +110,16 @@ func (t BlackholeTask) getHost(host string) ([]string, error) { func appendHosts(cmd []string, flag string, hosts ...string) []string { ips := "" for i, ip := range hosts { - if i > 0 { ips += ","} + if i > 0 { + ips += "," + } ips += ip } - + return append(cmd, "-d", ips) } -func (t BlackholeTask) rules() ([][]string, error) { +func (t TargetedBlockerTask) rules() ([][]string, error) { rules := [][]string{} for _, target := range t.opts.Targets { @@ -127,7 +129,7 @@ func (t BlackholeTask) rules() ([][]string, error) { var dsthosts []string var direction, protocol, dports, sports string - + srchosts, err := t.getHost(target.SrcHost) if err != nil { return nil, err @@ -179,9 +181,9 @@ func (t BlackholeTask) rules() ([][]string, error) { } else { return nil, bosherr.Errorf("Invalid destination port specified %v", target.SrcPorts) } - + cmd := []string{direction} - + if dsthosts != nil { cmd = appendHosts(cmd, "-d", dsthosts...) } @@ -197,20 +199,20 @@ func (t BlackholeTask) rules() ([][]string, error) { if dports != "" { cmd = append(cmd, "--dport", dports) } - + if sports != "" { cmd = append(cmd, "--sport", sports) } cmd = append(cmd, "-j", "DROP") - + rules = append(rules, cmd) } return rules, nil } -func (t BlackholeTask) dig(hostname string) ([]string, error) { +func (t TargetedBlockerTask) dig(hostname string) ([]string, error) { args := []string{"+short", hostname} output, _, _, err := t.cmdRunner.RunCommand("dig", args...) if err != nil { @@ -221,11 +223,11 @@ func (t BlackholeTask) dig(hostname string) ([]string, error) { if ips == nil { return nil, bosherr.Errorf("No IPs found for host %v", hostname) } - + return ips, nil } -func (t BlackholeTask) iptables(action string, rule []string) error { +func (t TargetedBlockerTask) iptables(action string, rule []string) error { args := append([]string{action}, rule...) _, _, _, err := t.cmdRunner.RunCommand("iptables", args...) From f6bad9dce9b9118801299ecda8674d5b705b0a96 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 30 Jan 2019 10:36:49 +0100 Subject: [PATCH 25/29] not required to use two chains --- .../cppforlife/turbulence/tasks/control_net_task.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index abfc276..b0c5a07 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -146,7 +146,7 @@ func (t ControlNetTask) configureInterface(ifaceName string, opts []string) erro } func (t ControlNetTask) configureBandwidth(ifaceName string) error { - _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "handle", "1:", "root", "htb", "default", "11") + _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "handle", "1:", "root", "htb", "default", "1") if err != nil { return err } @@ -157,12 +157,6 @@ func (t ControlNetTask) configureBandwidth(ifaceName string) error { return err } - _, _, _, err = t.cmdRunner.RunCommand("tc", "class", "add", "dev", ifaceName, "parent", "1:1", "classid", "1:11", "htb", "rate", t.opts.Bandwidth) - if err != nil { - t.resetIface(ifaceName) - return err - } - return nil } From c68ed294e28e86857144687b82ce22ee6d7c0d47 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 31 Jan 2019 17:31:51 +0100 Subject: [PATCH 26/29] add capability to specify outgoing destinations for ControlNetwork --- .../turbulence/tasks/control_net_task.go | 120 +++++++++++++++++- 1 file changed, 114 insertions(+), 6 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index b0c5a07..17a5f43 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -1,6 +1,8 @@ package tasks import ( + "regexp" + bosherr "github.com/cloudfoundry/bosh-utils/errors" boshlog "github.com/cloudfoundry/bosh-utils/logger" boshsys "github.com/cloudfoundry/bosh-utils/system" @@ -35,6 +37,19 @@ type ControlNetOptions struct { Bandwidth string // reset: tc qdisc del dev eth0 root + + // since tc is for egress only, specify which targets to affect + Targets []DestinationTarget +} + +type DestinationTarget struct { + // Optional destination host to block, can specify an address such as "10.34.4.60", an address block such as "192.168.0.0/24", + // or a domain name such as "google.com" which will be resolved to an Ip. + DstHost string + + + // Optional "dport" or destination port(s) to block. No range of ports is supported, as this is too dificult to implement via masking: https://serverfault.com/questions/231880/how-to-match-port-range-using-u32-filter + DstPort string } func (ControlNetOptions) _private() {} @@ -134,19 +149,23 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { } func (t ControlNetTask) configureInterface(ifaceName string, opts []string) error { - args := []string{"qdisc", "add", "dev", ifaceName, "root", "netem"} - args = append(args, opts...) + _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "root", "handle", "1:", "prio") + if err != nil { + return err + } - _, _, _, err := t.cmdRunner.RunCommand("tc", args...) + args := []string{"qdisc", "add", "dev", ifaceName, "parent", "1:1", "handle", "30:", "netem"} + args = append(args, opts...) + _, _, _, err = t.cmdRunner.RunCommand("tc", args...) if err != nil { - return bosherr.WrapError(err, "Shelling out to tc") + return err } - return nil + return t.configureDestination(ifaceName) } func (t ControlNetTask) configureBandwidth(ifaceName string) error { - _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "handle", "1:", "root", "htb", "default", "1") + _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "root", "handle", "1:", "htb") if err != nil { return err } @@ -157,6 +176,67 @@ func (t ControlNetTask) configureBandwidth(ifaceName string) error { return err } + return t.configureDestination(ifaceName) +} + +func (t ControlNetTask) configureDestination(ifaceName string) error { + rules := [][]string{} + + if len(t.opts.Targets) == 0 { + // we need to add this to forward the traffic to the default class + rules = [][]string{[]string{"match", "ip", "dst", "0.0.0.0/0"}} + } else { + for _, target := range t.opts.Targets { + if target.DstHost == "" && target.DstPort == "" { + return bosherr.Error("Must specify at least one of DstHost or DstPort.") + } + + var dsthosts []string + var dport string + + if target.DstPort == "" { + dport = "" + } else if destinationPortPattern.MatchString(target.DstPort) { + dport = target.DstPort + } else { + return bosherr.Errorf("Invalid destination port specified %v", target.DstPort) + } + + dsthosts, err := t.getHost(target.DstHost) + if err != nil { + return err + } + + if len(dsthosts) == 0 { + // only port was specified + rules = append(rules, []string{"match", "ip", "dport", dport, "0xffff"}) + } else { + for _, dsthost := range dsthosts { + args := []string{"match", "ip", "dst", dsthost} + + if (dport != "") { + // check if we have to add the port to the same rule + args = append(args, []string{"match", "ip", "dport", dport, "0xffff"}...) + } + + rules = append(rules, args) + } + } + } + } + + for _, rule := range rules { + args := []string{"filter", "add", "dev", ifaceName, "protocol", "ip", "parent", "1:", "prio", "1", "u32", "match"} + args = append(args, rule...) + args = append(args, []string{"flowid", "1:1"}...) + + _, _, _, err := t.cmdRunner.RunCommand("tc", args...) + if err != nil { + t.resetIface(ifaceName) + return err + } + } + return nil } @@ -168,3 +248,31 @@ func (t ControlNetTask) resetIface(ifaceName string) error { return nil } + +var destinationIpPattern = regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(/\d{0,2})?`) +var destinationPortPattern = regexp.MustCompile(`\d+(:\d+)?$`) + +func (t ControlNetTask) dig(hostname string) ([]string, error) { + args := []string{"+short", hostname} + output, _, _, err := t.cmdRunner.RunCommand("dig", args...) + if err != nil { + return nil, bosherr.WrapError(err, "resolving host name") + } + + ips := destinationIpPattern.FindAllString(output, -1) + if ips == nil { + return nil, bosherr.Errorf("No IPs found for host %v", hostname) + } + + return ips, nil +} + +func (t ControlNetTask) getHost(host string) ([]string, error) { + if host == "" { + return nil, nil + } else if destinationIpPattern.MatchString(host) { + return destinationIpPattern.FindAllString(host, -1), nil + } else { + return t.dig(host) + } +} From 5d1472e7a5301eb6723da8bbc3e92b5e7fb89d99 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 31 Jan 2019 20:30:34 +0100 Subject: [PATCH 27/29] fix bug --- src/github.com/cppforlife/turbulence/tasks/control_net_task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index 17a5f43..6e1b78d 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -226,7 +226,7 @@ func (t ControlNetTask) configureDestination(ifaceName string) error { } for _, rule := range rules { - args := []string{"filter", "add", "dev", ifaceName, "protocol", "ip", "parent", "1:", "prio", "1", "u32", "match"} + args := []string{"filter", "add", "dev", ifaceName, "protocol", "ip", "parent", "1:", "prio", "1", "u32"} args = append(args, rule...) args = append(args, []string{"flowid", "1:1"}...) From 2e8fbdfb1af86251eb72690393cb28eb64e8dea7 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 8 Mar 2019 09:25:09 +0100 Subject: [PATCH 28/29] added controlnet target documentation --- docs/api.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 0b51637..34dfd6a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -366,6 +366,9 @@ One or both of the following configurations must be selected: - set `Bandwidth` (string; required). Must be suffixed with one of `kbps`, `mbps` or `gbps`. - bandwidth limiting must be used without any other effects. +In addition it is possible to apply a destination filter: + - set `Targets` (array, optional). Must include either `DstHost` or `DstPort` + Example: ```json @@ -373,7 +376,14 @@ Example: "Type": "ControlNet", "Timeout": "10m", // Times may be suffixed with ms,s,m,h - "Delay": "50ms" + "Delay": "50ms", + + "Targets": [ + { + "DstHost": "1.2.3.4", + "DstHost": "443" + } + ] } ``` From 794cbdabe69ed2985ff534cfc2842937cdc22cce Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 15 Mar 2019 15:52:05 +0100 Subject: [PATCH 29/29] reset all interfaces in case of error to avoid subsequent errors caused by re-executions of the same command for working interfaces --- .../turbulence/tasks/control_net_task.go | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index 6e1b78d..fb18181 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -2,6 +2,7 @@ package tasks import ( "regexp" + "strings" bosherr "github.com/cloudfoundry/bosh-utils/errors" boshlog "github.com/cloudfoundry/bosh-utils/logger" @@ -96,6 +97,7 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { for _, ifaceName := range ifaceNames { err := t.configureBandwidth(ifaceName) if err != nil { + t.resetIfaces(ifaceNames) return err } } @@ -128,6 +130,7 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { for _, ifaceName := range ifaceNames { err := t.configureInterface(ifaceName, opts) if err != nil { + t.resetIfaces(ifaceNames) return err } } @@ -138,14 +141,8 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { case <-stopCh: } - for _, ifaceName := range ifaceNames { - err := t.resetIface(ifaceName) - if err != nil { - return err - } - } - return nil + return t.resetIfaces(ifaceNames) } func (t ControlNetTask) configureInterface(ifaceName string, opts []string) error { @@ -172,7 +169,6 @@ func (t ControlNetTask) configureBandwidth(ifaceName string) error { _, _, _, err = t.cmdRunner.RunCommand("tc", "class", "add", "dev", ifaceName, "parent", "1:", "classid", "1:1", "htb", "rate", t.opts.Bandwidth) if err != nil { - t.resetIface(ifaceName) return err } @@ -232,7 +228,6 @@ func (t ControlNetTask) configureDestination(ifaceName string) error { _, _, _, err := t.cmdRunner.RunCommand("tc", args...) if err != nil { - t.resetIface(ifaceName) return err } } @@ -240,10 +235,22 @@ func (t ControlNetTask) configureDestination(ifaceName string) error { return nil } -func (t ControlNetTask) resetIface(ifaceName string) error { - _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "del", "dev", ifaceName, "root") - if err != nil { - return bosherr.WrapError(err, "Resetting tc") +func (t ControlNetTask) resetIfaces(ifaceNames []string) error { + errors := []error{} + for _, ifaceName := range ifaceNames { + _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "del", "dev", ifaceName, "root") + if err != nil { + errors = append(errors, err) + } + } + + if len(errors) != 0 { + msgs := []string{} + for _, error := range errors { + msgs = append(msgs, error.Error()) + } + + return bosherr.Errorf("Errors detected during reset: %s", strings.Join(msgs," ")) } return nil