diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcadb2c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/docs/api.md b/docs/api.md index 26d932b..34dfd6a 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 { @@ -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. @@ -263,6 +282,58 @@ Example: } ``` +### 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. + +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 `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 `SrcHost`, `DstHost`, `DstPorts`, or `SrcPorts` must be specified. + +Example: + +```json +{ + "Type": "TargetedBlocker", + "Timeout": "10m", // Times may be suffixed with ms,s,m,h + "Targets": [{ + "DstHost": "1.1.1.1", + "Direction": "INPUT", + "DstPorts": "53" + },{ + "DstHost": "google.com", + "Direction": "FORWARD", + "Protocol": "tcp", + "DstPorts": "80" + }] +} +``` + + +### 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`. @@ -274,10 +345,29 @@ 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. + +- 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. + +In addition it is possible to apply a destination filter: + - set `Targets` (array, optional). Must include either `DstHost` or `DstPort` Example: @@ -286,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" + } + ] } ``` @@ -300,12 +397,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/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 diff --git a/src/github.com/cppforlife/turbulence/agent/agent.go b/src/github.com/cppforlife/turbulence/agent/agent.go index cf204f0..75352a9 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) @@ -155,6 +158,12 @@ 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.TargetedBlockerOptions: + t = tasks.NewTargetedBlockerTask(a.cmdRunner, opts, 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/control_net_task.go b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go index d660469..fb18181 100644 --- a/src/github.com/cppforlife/turbulence/tasks/control_net_task.go +++ b/src/github.com/cppforlife/turbulence/tasks/control_net_task.go @@ -1,12 +1,16 @@ 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" ) // 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,7 +23,34 @@ 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 + + // 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() {} @@ -33,14 +64,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,31 +93,44 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { return err } - if len(t.opts.Delay) > 0 { - variation := t.opts.DelayVariation - - if len(variation) == 0 { - variation = "10ms" - } - + if bandwidth { for _, ifaceName := range ifaceNames { - err := t.configureDelay(ifaceName, t.opts.Delay, variation) + err := t.configureBandwidth(ifaceName) if err != nil { + t.resetIfaces(ifaceNames) return err } } - } + } else { + opts := make([]string, 0, 16) - if len(t.opts.Loss) > 0 { - correlation := t.opts.LossCorrelation + 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 len(correlation) == 0 { - correlation = "75%" + 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) } for _, ifaceName := range ifaceNames { - err := t.configurePacketLoss(ifaceName, t.opts.Loss, correlation) + err := t.configureInterface(ifaceName, opts) if err != nil { + t.resetIfaces(ifaceNames) return err } } @@ -83,49 +141,145 @@ func (t ControlNetTask) Execute(stopCh chan struct{}) error { case <-stopCh: } - for _, ifaceName := range ifaceNames { - err := t.resetIface(ifaceName) - if err != nil { - return err - } + + return t.resetIfaces(ifaceNames) +} + +func (t ControlNetTask) configureInterface(ifaceName string, opts []string) error { + _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "root", "handle", "1:", "prio") + if err != nil { + return err } - return nil + 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 err + } + + return t.configureDestination(ifaceName) } -func (t ControlNetTask) configureDelay(ifaceName, delay, variation string) error { - args := []string{ - "qdisc", "add", "dev", ifaceName, "root", - "netem", "delay", delay, variation, "distribution", "normal", +func (t ControlNetTask) configureBandwidth(ifaceName string) error { + _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "add", "dev", ifaceName, "root", "handle", "1:", "htb") + if err != nil { + return err } - _, _, _, err := t.cmdRunner.RunCommand("tc", args...) + _, _, _, err = t.cmdRunner.RunCommand("tc", "class", "add", "dev", ifaceName, "parent", "1:", "classid", "1:1", "htb", "rate", t.opts.Bandwidth) if err != nil { - return bosherr.WrapError(err, "Shelling out to tc to add delay") + 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"} + args = append(args, rule...) + args = append(args, []string{"flowid", "1:1"}...) + + _, _, _, err := t.cmdRunner.RunCommand("tc", args...) + if err != nil { + return err + } } 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) 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) + } } - _, _, _, err := t.cmdRunner.RunCommand("tc", args...) - if err != nil { - return bosherr.WrapError(err, "Shelling out to tc to add packet loss") + 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 } -func (t ControlNetTask) resetIface(ifaceName string) error { - _, _, _, err := t.cmdRunner.RunCommand("tc", "qdisc", "del", "dev", ifaceName, "root") +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 bosherr.WrapError(err, "Resetting tc") + return nil, bosherr.WrapError(err, "resolving host name") } - return nil + 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) + } } 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..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,13 +1,14 @@ 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" ) type FillDiskOptions struct { Type string + Timeout string // todo to percentage @@ -22,33 +23,64 @@ 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 { + 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 t.opts.Ephemeral { - return t.fill("/var/vcap/data/.filler") + if err != nil { + return err } - if t.opts.Temporary { - return t.fill("/tmp/.filler") + select { + case <-stopCh: + case <-timeoutCh: } - return t.fill("/.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 err } func (t FillDiskTask) fill(path string) error { _, _, _, err := t.cmdRunner.RunCommand("dd", "if=/dev/zero", "of="+path, "bs=1M") if err != nil { - return bosherr.WrapError(err, "Filling disk") + 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 } + +func (t FillDiskTask) remove(path string) error { + _, _, _, err := t.cmdRunner.RunCommand("rm", path) + return err +} diff --git a/src/github.com/cppforlife/turbulence/tasks/options.go b/src/github.com/cppforlife/turbulence/tasks/options.go index fc6e456..b8260af 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 @@ -56,6 +60,14 @@ func (s *OptionsSlice) UnmarshalJSON(data []byte) error { var o FirewallOptions err, opts = json.Unmarshal(bytes, &o), o + case optType == OptionsType(TargetedBlockerOptions{}): + var o TargetedBlockerOptions + 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 @@ -96,6 +108,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 @@ -108,6 +124,14 @@ func (s OptionsSlice) MarshalJSON() ([]byte, error) { typedO.Type = OptionsType(typedO) s[i] = typedO + case TargetedBlockerOptions: + 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 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/targeted_blocker_task.go b/src/github.com/cppforlife/turbulence/tasks/targeted_blocker_task.go new file mode 100644 index 0000000..7664d0d --- /dev/null +++ b/src/github.com/cppforlife/turbulence/tasks/targeted_blocker_task.go @@ -0,0 +1,239 @@ +// 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 TargetedBlockerOptions struct { + Type string + Timeout string // Times may be suffixed with ms,s,m,h + + 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+)?$`) + +// 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 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 + + // 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 + + // 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 (TargetedBlockerOptions) _private() {} + +type TargetedBlockerTask struct { + cmdRunner boshsys.CmdRunner + opts TargetedBlockerOptions + logger boshlog.Logger +} + +func NewTargetedBlockerTask( + cmdRunner boshsys.CmdRunner, + opts TargetedBlockerOptions, + logger boshlog.Logger, +) TargetedBlockerTask { + return TargetedBlockerTask{cmdRunner, opts, logger} +} + +func (t TargetedBlockerTask) 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 _, 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 + } + } + + select { + case <-timeoutCh: + case <-stopCh: + } + + for _, r := range rules { + err := t.iptables("-D", r) + if err != nil { + return err + } + } + + return nil +} + +func (t TargetedBlockerTask) 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 TargetedBlockerTask) rules() ([][]string, error) { + rules := [][]string{} + + for _, target := range t.opts.Targets { + 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 dsthosts []string + var direction, protocol, dports, sports string + + 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) { + case "INPUT": + direction = "INPUT" + case "OUTPUT": + direction = "OUTPUT" + case "FORWARD": + direction = "FORWARD" + default: + return nil, bosherr.Errorf("Invalid direction '%v', must be one of {INPUT, OUTPUT, FORWARD}.", target.Direction) + } + + switch strings.ToLower(target.Protocol) { + case "": + protocol = "" + 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.Protocol) + } + + 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) + } + + cmd := []string{direction} + + if dsthosts != nil { + cmd = appendHosts(cmd, "-d", dsthosts...) + } + + if srchosts != nil { + cmd = appendHosts(cmd, "-s", srchosts...) + } + + if protocol != "" { + cmd = append(cmd, "-p", protocol) + } + + 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 TargetedBlockerTask) 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 TargetedBlockerTask) iptables(action string, rule []string) error { + args := append([]string{action}, 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/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 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)