Skip to content

Commit

Permalink
Added ssh connection rate limiting feature
Browse files Browse the repository at this point in the history
- allows enabling ssh connection rate limiting
- adds configurable amount of max connections per duration window
- adds configurable duration window
  • Loading branch information
dbathgate committed May 8, 2024
1 parent 09c9679 commit 952633b
Show file tree
Hide file tree
Showing 40 changed files with 378 additions and 77 deletions.
5 changes: 3 additions & 2 deletions bosh/build_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bosh

import (
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/instance"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ratelimiter"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ssh"
"github.com/cloudfoundry/bosh-cli/v7/director"
"github.com/pkg/errors"
Expand All @@ -10,7 +11,7 @@ import (
boshlog "github.com/cloudfoundry/bosh-utils/logger"
)

func BuildClient(targetUrl, username, password, caCert, bbrVersion string, logger boshlog.Logger) (Client, error) {
func BuildClient(targetUrl, username, password, caCert, bbrVersion string, rateLimiter ratelimiter.RateLimiter, logger boshlog.Logger) (Client, error) {
var client Client

factoryConfig, err := director.NewConfigFromURL(targetUrl)
Expand Down Expand Up @@ -44,7 +45,7 @@ func BuildClient(targetUrl, username, password, caCert, bbrVersion string, logge
return client, errors.Wrap(err, "error building bosh director client")
}

return NewClient(boshDirector, director.NewSSHOpts, ssh.NewSshRemoteRunner, logger, instance.NewJobFinder(bbrVersion, logger), NewBoshManifestQuerier), nil
return NewClient(boshDirector, director.NewSSHOpts, ssh.NewSshRemoteRunner, rateLimiter, logger, instance.NewJobFinder(bbrVersion, logger), NewBoshManifestQuerier), nil
}

func getDirectorInfo(directorFactory director.Factory, factoryConfig director.FactoryConfig) (director.Info, error) {
Expand Down
13 changes: 7 additions & 6 deletions bosh/build_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/internal/cf-webmock/mockbosh"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/internal/cf-webmock/mockhttp"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/internal/cf-webmock/mockuaa"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ratelimiter"
boshlog "github.com/cloudfoundry/bosh-utils/logger"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -50,7 +51,7 @@ var _ = Describe("BuildClient", func() {
mockbosh.Manifest(deploymentName).RespondsWith([]byte("manifest contents")),
)

client, err := BuildClient(director.URL, username, password, caCert, bbrVersion, logger)
client, err := BuildClient(director.URL, username, password, caCert, bbrVersion, ratelimiter.NoOpRateLimiter{}, logger)

Expect(err).NotTo(HaveOccurred())
manifest, err := client.GetManifest(deploymentName)
Expand All @@ -75,7 +76,7 @@ var _ = Describe("BuildClient", func() {
mockbosh.Manifest(deploymentName).RespondsWith([]byte("manifest contents")),
)

client, err := BuildClient(director.URL, username, password, caCert, bbrVersion, logger)
client, err := BuildClient(director.URL, username, password, caCert, bbrVersion, ratelimiter.NoOpRateLimiter{}, logger)

Expect(err).NotTo(HaveOccurred())
manifest, err := client.GetManifest(deploymentName)
Expand All @@ -90,7 +91,7 @@ var _ = Describe("BuildClient", func() {
director.VerifyAndMock(
mockbosh.Info().WithAuthTypeUAA(""),
)
_, err := BuildClient(director.URL, username, password, caCert, bbrVersion, logger)
_, err := BuildClient(director.URL, username, password, caCert, bbrVersion, ratelimiter.NoOpRateLimiter{}, logger)

Expect(err).To(MatchError(ContainSubstring("invalid UAA URL")))

Expand All @@ -103,7 +104,7 @@ var _ = Describe("BuildClient", func() {
caCertPath := "-----BEGIN"
basicAuthDirectorURL := director.URL

_, err := BuildClient(basicAuthDirectorURL, username, password, caCertPath, bbrVersion, logger)
_, err := BuildClient(basicAuthDirectorURL, username, password, caCertPath, bbrVersion, ratelimiter.NoOpRateLimiter{}, logger)
Expect(err).To(MatchError(ContainSubstring("Missing PEM block")))
})

Expand All @@ -113,7 +114,7 @@ var _ = Describe("BuildClient", func() {
caCertPath := ""
basicAuthDirectorURL := ""

_, err := BuildClient(basicAuthDirectorURL, username, password, caCertPath, bbrVersion, logger)
_, err := BuildClient(basicAuthDirectorURL, username, password, caCertPath, bbrVersion, ratelimiter.NoOpRateLimiter{}, logger)
Expect(err).To(MatchError(ContainSubstring("invalid bosh URL")))
})

Expand All @@ -125,7 +126,7 @@ var _ = Describe("BuildClient", func() {
mockbosh.Info().Fails("fooo!"),
)

_, err := BuildClient(director.URL, username, password, caCert, bbrVersion, logger)
_, err := BuildClient(director.URL, username, password, caCert, bbrVersion, ratelimiter.NoOpRateLimiter{}, logger)
Expect(err).To(MatchError(ContainSubstring("bosh director unreachable or unhealthy")))
})
})
6 changes: 5 additions & 1 deletion bosh/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/cloudfoundry-incubator/bosh-backup-and-restore/instance"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/orchestrator"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ratelimiter"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ssh"
"github.com/cloudfoundry/bosh-cli/v7/director"
"github.com/cloudfoundry/bosh-utils/uuid"
Expand All @@ -25,13 +26,15 @@ type BoshClient interface {
func NewClient(boshDirector director.Director,
sshOptsGenerator ssh.SSHOptsGenerator,
remoteRunnerFactory ssh.RemoteRunnerFactory,
rateLimiter ratelimiter.RateLimiter,
logger Logger,
jobFinder instance.JobFinder,
manifestQuerierCreator instance.ManifestQuerierCreator) Client {
return Client{
Director: boshDirector,
SSHOptsGenerator: sshOptsGenerator,
RemoteRunnerFactory: remoteRunnerFactory,
RateLimiter: rateLimiter,
Logger: logger,
jobFinder: jobFinder,
manifestQuerierCreator: manifestQuerierCreator,
Expand All @@ -42,6 +45,7 @@ type Client struct {
director.Director
ssh.SSHOptsGenerator
ssh.RemoteRunnerFactory
ratelimiter.RateLimiter
Logger
jobFinder instance.JobFinder
manifestQuerierCreator instance.ManifestQuerierCreator
Expand Down Expand Up @@ -112,7 +116,7 @@ func (c Client) FindInstances(deploymentName string) ([]orchestrator.Instance, e
return nil, errors.Wrap(err, "ssh.NewConnection.ParseAuthorizedKey failed")
}

remoteRunner, err := c.RemoteRunnerFactory(host.Host, host.Username, privateKey, gossh.FixedHostKey(hostPublicKey), supportedEncryptionAlgorithms(hostPublicKey), c.Logger)
remoteRunner, err := c.RemoteRunnerFactory(host.Host, host.Username, privateKey, gossh.FixedHostKey(hostPublicKey), supportedEncryptionAlgorithms(hostPublicKey), c.RateLimiter, c.Logger)
if err != nil {
cleanupAlreadyMadeConnections(deployment, slugs, sshOpts)
return nil, errors.Wrap(err, "failed to connect using ssh")
Expand Down
21 changes: 11 additions & 10 deletions bosh/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/instance"
instancefakes "github.com/cloudfoundry-incubator/bosh-backup-and-restore/instance/fakes"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/orchestrator"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ratelimiter"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ssh"
sshfakes "github.com/cloudfoundry-incubator/bosh-backup-and-restore/ssh/fakes"
"github.com/cloudfoundry/bosh-cli/v7/director"
Expand Down Expand Up @@ -43,7 +44,7 @@ var _ = Describe("Director", func() {
var b bosh.BoshClient

JustBeforeEach(func() {
b = bosh.NewClient(boshDirector, optsGenerator.Spy, remoteRunnerFactory.Spy, boshLogger, fakeJobFinder, manifestQuerierCreator.Spy)
b = bosh.NewClient(boshDirector, optsGenerator.Spy, remoteRunnerFactory.Spy, ratelimiter.NoOpRateLimiter{}, boshLogger, fakeJobFinder, manifestQuerierCreator.Spy)
})

BeforeEach(func() {
Expand Down Expand Up @@ -166,7 +167,7 @@ var _ = Describe("Director", func() {

It("creates a remote runner for each host", func() {
Expect(remoteRunnerFactory.CallCount()).To(Equal(1))
host, username, privateKey, _, hostPublicKeyAlgorithm, logger := remoteRunnerFactory.ArgsForCall(0)
host, username, privateKey, _, hostPublicKeyAlgorithm, _, logger := remoteRunnerFactory.ArgsForCall(0)
Expect(host).To(Equal("10.0.0.0"))
Expect(username).To(Equal("username"))
Expect(privateKey).To(Equal("private_key"))
Expand Down Expand Up @@ -195,7 +196,7 @@ var _ = Describe("Director", func() {

It("uses the specified port", func() {
Expect(remoteRunnerFactory.CallCount()).To(Equal(1))
host, username, privateKey, _, hostPublicKeyAlgorithm, logger := remoteRunnerFactory.ArgsForCall(0)
host, username, privateKey, _, hostPublicKeyAlgorithm, _, logger := remoteRunnerFactory.ArgsForCall(0)
Expect(host).To(Equal("10.0.0.0:3457"))
Expect(username).To(Equal("username"))
Expect(privateKey).To(Equal("private_key"))
Expand Down Expand Up @@ -328,14 +329,14 @@ var _ = Describe("Director", func() {
It("creates a remote runner for each host", func() {
Expect(remoteRunnerFactory.CallCount()).To(Equal(2))

host, username, privateKey, _, hostPublicKeyAlgorithm, logger := remoteRunnerFactory.ArgsForCall(0)
host, username, privateKey, _, hostPublicKeyAlgorithm, _, logger := remoteRunnerFactory.ArgsForCall(0)
Expect(host).To(Equal("10.0.0.1"))
Expect(username).To(Equal("username"))
Expect(privateKey).To(Equal("private_key"))
Expect(hostPublicKeyAlgorithm).To(Equal(hostKeyAlgorithmRSA))
Expect(logger).To(Equal(boshLogger))

host, username, privateKey, _, hostPublicKeyAlgorithm, logger = remoteRunnerFactory.ArgsForCall(1)
host, username, privateKey, _, hostPublicKeyAlgorithm, _, logger = remoteRunnerFactory.ArgsForCall(1)
Expect(host).To(Equal("10.0.0.2"))
Expect(username).To(Equal("username"))
Expect(privateKey).To(Equal("private_key"))
Expand Down Expand Up @@ -621,21 +622,21 @@ var _ = Describe("Director", func() {
It("creates a remote runner for each host that has scripts, and the first instance of each group that doesn't", func() {
Expect(remoteRunnerFactory.CallCount()).To(Equal(3))

host, username, privateKey, _, hostPublicKeyAlgorithm, logger := remoteRunnerFactory.ArgsForCall(0)
host, username, privateKey, _, hostPublicKeyAlgorithm, _, logger := remoteRunnerFactory.ArgsForCall(0)
Expect(host).To(Equal("10.0.0.1"))
Expect(username).To(Equal("username"))
Expect(privateKey).To(Equal("private_key"))
Expect(hostPublicKeyAlgorithm).To(Equal(hostKeyAlgorithmRSA))
Expect(logger).To(Equal(boshLogger))

host, username, privateKey, _, hostPublicKeyAlgorithm, logger = remoteRunnerFactory.ArgsForCall(1)
host, username, privateKey, _, hostPublicKeyAlgorithm, _, logger = remoteRunnerFactory.ArgsForCall(1)
Expect(host).To(Equal("10.0.0.3"))
Expect(username).To(Equal("username"))
Expect(privateKey).To(Equal("private_key"))
Expect(hostPublicKeyAlgorithm).To(Equal(hostKeyAlgorithmRSA))
Expect(logger).To(Equal(boshLogger))

host, username, privateKey, _, hostPublicKeyAlgorithm, logger = remoteRunnerFactory.ArgsForCall(2)
host, username, privateKey, _, hostPublicKeyAlgorithm, _, logger = remoteRunnerFactory.ArgsForCall(2)
Expect(host).To(Equal("10.0.0.4"))
Expect(username).To(Equal("username"))
Expect(privateKey).To(Equal("private_key"))
Expand Down Expand Up @@ -689,7 +690,7 @@ var _ = Describe("Director", func() {

It("uses the ECDSA algorithm to create its remote runners", func() {
Expect(remoteRunnerFactory.CallCount()).To(Equal(1))
_, _, _, _, hostPublicKeyAlgorithm, _ := remoteRunnerFactory.ArgsForCall(0)
_, _, _, _, hostPublicKeyAlgorithm, _, _ := remoteRunnerFactory.ArgsForCall(0)
Expect(hostPublicKeyAlgorithm).To(Equal(hostKeyAlgorithmECDSA))
})

Expand Down Expand Up @@ -995,7 +996,7 @@ var _ = Describe("Director", func() {
}}, nil
}

remoteRunnerFactory.Stub = func(host, user, privateKey string, publicKeyCallback gossh.HostKeyCallback, publicKeyAlgorithm []string, logger ssh.Logger) (ssh.RemoteRunner, error) {
remoteRunnerFactory.Stub = func(host, user, privateKey string, publicKeyCallback gossh.HostKeyCallback, publicKeyAlgorithm []string, rateLimiter ratelimiter.RateLimiter, logger ssh.Logger) (ssh.RemoteRunner, error) {
if host == "10.0.0.0_job1" {
return remoteRunner, nil
}
Expand Down
13 changes: 13 additions & 0 deletions cli/command/all_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/executor/deployment"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/factory"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/orchestrator"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ratelimiter"
"github.com/urfave/cli"
)

Expand Down Expand Up @@ -128,6 +129,18 @@ func getDeploymentParams(c *cli.Context) (string, string, string, string, string
return username, password, target, caCert, bbrVersion, debug, deployment, allDeployments
}

func getConnectionRateLimiter(c *cli.Context) (ratelimiter.RateLimiter, error) {
enabled := c.Parent().Bool("rate-limiting")
maxConnections := c.Parent().Int("rate-limiting-max-connections")
durationSeconds := c.Parent().Int("rate-limiting-duration-seconds")

if enabled {
return ratelimiter.NewConnectionRateLimiter(maxConnections, durationSeconds)
}
return ratelimiter.NewNoOpRateLimiter(), nil

}

type DeploymentExecutable struct {
action ActionFunc
name string
Expand Down
20 changes: 14 additions & 6 deletions cli/command/deployment_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/executor/deployment"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/factory"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/orchestrator"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ratelimiter"
"github.com/urfave/cli"
)

Expand Down Expand Up @@ -50,17 +51,23 @@ func (d DeploymentBackupCommand) Action(c *cli.Context) error {
unsafeLockFree := c.Bool("unsafe-lock-free")
artifactPath := c.String("artifact-path")

rateLimiter, err := getConnectionRateLimiter(c)

if err != nil {
return err
}

if allDeployments {
if unsafeLockFree {
return processError(orchestrator.NewError(fmt.Errorf("Cannot use the --unsafe-lock-free flag in conjunction with the --all-deployments flag")))
}
return backupAll(target, username, password, caCert, artifactPath, withManifest, bbrVersion, debug)
return backupAll(target, username, password, caCert, artifactPath, withManifest, bbrVersion, debug, rateLimiter)
}

return backupSingleDeployment(deployment, target, username, password, caCert, artifactPath, withManifest, bbrVersion, unsafeLockFree, debug)
return backupSingleDeployment(deployment, target, username, password, caCert, artifactPath, withManifest, bbrVersion, unsafeLockFree, debug, rateLimiter)
}

func backupAll(target, username, password, caCert, artifactPath string, withManifest bool, bbrVersion string, debug bool) error {
func backupAll(target, username, password, caCert, artifactPath string, withManifest bool, bbrVersion string, debug bool, rateLimiter ratelimiter.RateLimiter) error {
backupAction := func(deploymentName string) orchestrator.Error {
timestamp := time.Now().UTC().Format(artifactTimeStampFormat)
logFilePath, buffer, logger, logErr := createLogger(timestamp, artifactPath, deploymentName, debug)
Expand All @@ -76,6 +83,7 @@ func backupAll(target, username, password, caCert, artifactPath string, withMani
withManifest,
false,
bbrVersion,
rateLimiter,
logger,
timestamp,
)
Expand Down Expand Up @@ -106,7 +114,7 @@ func backupAll(target, username, password, caCert, artifactPath string, withMani
fmt.Println("Starting backup...")

logger, _ := factory.BuildBoshLoggerWithCustomBuffer(debug)
boshClient, err := factory.BuildBoshClient(target, username, password, caCert, bbrVersion, logger)
boshClient, err := factory.BuildBoshClient(target, username, password, caCert, bbrVersion, rateLimiter, logger)
if err != nil {
return processError(orchestrator.NewError(err))
}
Expand All @@ -119,11 +127,11 @@ func backupAll(target, username, password, caCert, artifactPath string, withMani
deployment.NewParallelExecutor())
}

func backupSingleDeployment(deployment, target, username, password, caCert, artifactPath string, withManifest bool, bbrVersion string, unsafeLockFree, debug bool) error {
func backupSingleDeployment(deployment, target, username, password, caCert, artifactPath string, withManifest bool, bbrVersion string, unsafeLockFree, debug bool, rateLimiter ratelimiter.RateLimiter) error {
logger := factory.BuildBoshLogger(debug)
timeStamp := time.Now().UTC().Format(artifactTimeStampFormat)

backuper, err := factory.BuildDeploymentBackuper(target, username, password, caCert, withManifest, unsafeLockFree, bbrVersion, logger, timeStamp)
backuper, err := factory.BuildDeploymentBackuper(target, username, password, caCert, withManifest, unsafeLockFree, bbrVersion, rateLimiter, logger, timeStamp)
if err != nil {
return processError(orchestrator.NewError(err))
}
Expand Down
15 changes: 12 additions & 3 deletions cli/command/deployment_backup_cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/cloudfoundry-incubator/bosh-backup-and-restore/executor/deployment"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/ratelimiter"

"github.com/cloudfoundry-incubator/bosh-backup-and-restore/factory"
"github.com/cloudfoundry-incubator/bosh-backup-and-restore/orchestrator"
Expand All @@ -31,6 +32,12 @@ func (d DeploymentBackupCleanupCommand) Action(c *cli.Context) error {

username, password, target, caCert, bbrVersion, debug, deployment, allDeployments := getDeploymentParams(c)

rateLimiter, err := getConnectionRateLimiter(c)

if err != nil {
return err
}

if !allDeployments {
logger := factory.BuildBoshLogger(debug)

Expand All @@ -40,6 +47,7 @@ func (d DeploymentBackupCleanupCommand) Action(c *cli.Context) error {
password,
caCert,
c.App.Version,
rateLimiter,
logger,
)
if err != nil {
Expand All @@ -50,10 +58,10 @@ func (d DeploymentBackupCleanupCommand) Action(c *cli.Context) error {
return processError(cleanupErr)
}

return cleanupAllDeployments(target, username, password, caCert, bbrVersion, debug)
return cleanupAllDeployments(target, username, password, caCert, bbrVersion, debug, rateLimiter)
}

func cleanupAllDeployments(target, username, password, caCert, bbrVersion string, debug bool) error {
func cleanupAllDeployments(target, username, password, caCert, bbrVersion string, debug bool, rateLimiter ratelimiter.RateLimiter) error {
cleanupAction := func(deploymentName string) orchestrator.Error {
timestamp := time.Now().UTC().Format(artifactTimeStampFormat)
logFilePath, buffer, logger, logErr := createLogger(timestamp, "", deploymentName, debug)
Expand All @@ -67,6 +75,7 @@ func cleanupAllDeployments(target, username, password, caCert, bbrVersion string
password,
caCert,
bbrVersion,
rateLimiter,
logger,
)

Expand All @@ -93,7 +102,7 @@ func cleanupAllDeployments(target, username, password, caCert, bbrVersion string

logger, _ := factory.BuildBoshLoggerWithCustomBuffer(debug)

boshClient, err := factory.BuildBoshClient(target, username, password, caCert, bbrVersion, logger)
boshClient, err := factory.BuildBoshClient(target, username, password, caCert, bbrVersion, rateLimiter, logger)
if err != nil {
return err
}
Expand Down
9 changes: 8 additions & 1 deletion cli/command/deployment_pre_backup_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@ func (d DeploymentPreBackupCheck) Cli() cli.Command {

func (d DeploymentPreBackupCheck) Action(c *cli.Context) error {
username, password, target, caCert, bbrVersion, debug, deployment, allDeployments := getDeploymentParams(c)

rateLimiter, err := getConnectionRateLimiter(c)

if err != nil {
return err
}

var logger logger.Logger
if allDeployments {
logger, _ = factory.BuildBoshLoggerWithCustomBuffer(debug)
} else {
logger = factory.BuildBoshLogger(debug)
}
boshClient, err := factory.BuildBoshClient(target, username, password, caCert, bbrVersion, logger)
boshClient, err := factory.BuildBoshClient(target, username, password, caCert, bbrVersion, rateLimiter, logger)
if err != nil {
return processError(orchestrator.NewError(err))
}
Expand Down
Loading

0 comments on commit 952633b

Please sign in to comment.