Skip to content

Commit 54df359

Browse files
authored
Merge pull request #18 from luhring/same-subnet-int-tests
Total revision of analysis and set logic
2 parents 9e877ee + 89edcb6 commit 54df359

File tree

95 files changed

+6570
-1792
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+6570
-1792
lines changed

.circleci/config.yml

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,42 @@ version: 2
55
jobs:
66
build:
77
docker:
8-
- image: circleci/golang:1.11
8+
- image: circleci/golang:1.13.1
9+
working_directory: ~/repo
10+
steps:
11+
- checkout
12+
- run: CGO_ENABLED=0 go build -a -o "./build/reach"
913

14+
unit_tests:
15+
docker:
16+
- image: circleci/golang:1.13.1
1017
working_directory: ~/repo
18+
steps:
19+
- checkout
20+
- run: go test -cover ./reach
1121

22+
acceptance_tests:
23+
docker:
24+
- image: circleci/golang:1.13.1
25+
working_directory: ~/repo
1226
steps:
1327
- checkout
28+
- run: curl -sS https://releases.hashicorp.com/terraform/0.12.7/terraform_0.12.7_linux_amd64.zip -o ./terraform.zip && unzip terraform.zip && sudo mv terraform /usr/local/bin/ && which terraform
29+
- run: go test -cover ./reach -acceptance -log-tf -test.v
1430

15-
- run: go build -a -o "./build/reach"
16-
- run: go test -v -cover ./...
31+
workflows:
32+
version: 2
33+
commit:
34+
jobs:
35+
- build
36+
- unit_tests
37+
nightly:
38+
triggers:
39+
- schedule:
40+
cron: "0 0 * * *"
41+
filters:
42+
branches:
43+
only:
44+
- master
45+
jobs:
46+
- acceptance_tests

.data/reach-demo.gif

2.35 MB
Loading

README.md

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,115 @@
11
# reach
22

3-
A tool for examining network reachability issues in AWS.
4-
53
[![CircleCI](https://circleci.com/gh/luhring/reach.svg?style=svg)](https://circleci.com/gh/luhring/reach)
6-
[![Go Report Card](https://goreportcard.com/badge/github.com/luhring/reach)](https://goreportcard.com/report/github.com/luhring/reach)
74
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/luhring/reach/blob/master/LICENSE)
85

9-
**IMPORTANT: THIS IS STILL IN DEVELOPMENT! USE AT YOUR OWN RISK!**
6+
Reach is a tool for discovering the impact your AWS configuration has on the flow of network traffic.
107

11-
## Overview
8+
## Getting Started
129

13-
reach evaluates the potential for network connectivity between EC2 instances by querying the AWS API for network configuration data.
10+
To perform an analysis, specify a **source** EC2 instance and a **destination** EC2 instance:
1411

15-
reach determines the ports on an EC2 instance that can be accessed by another EC2 instance, taking into consideration security group rules, instance subnet placements, instance running states, network ACL rules, and route tables.
12+
```Text
13+
$ reach <source> <destination>
14+
```
1615

17-
reach doesn't need to run on any EC2 instance, it just needs to run on a system that has access to the AWS API.
16+
![Image](.data/reach-demo.gif)
1817

19-
**Disclaimer:** Because reach gets all of its information from the AWS API, reach makes no guarantees about network service accessibility with respect to the operating system or applications running on an EC2 instance. In other words, reach can tell you if your VPC resources and EC2 instances are configured correctly, but reach _can't_ tell you if an OS firewall is blocking network traffic, or if an application listening on a port has crashed.
18+
Reach uses your AWS configuration to analyze the potential for network connectivity between two EC2 instances in your AWS account. This means **you don't need to install Reach on any servers**you just need access to the AWS API.
2019

21-
## Uses
20+
The key benefits of Reach are:
2221

23-
You can ask what ports are reachable on an EC2 instance from the perspective of another instance.
22+
- **Solve problems faster:** Find missing links in a network path in _seconds_, not hours.
2423

25-
```ShellSession
26-
$ reach "client instance" "server instance"
27-
✔ TCP 80
28-
✔ TCP 443
29-
```
24+
- **Don't compromise on security:** Secure your network without worrying about impacting any required network flows.
25+
26+
- **Learn about your network:** Gain better insight into currently allowed network flows, and discover new consequences of your network design.
27+
28+
- **Build better pipelines:** Discover network-level problems before running application integration or end-to-end tests by adding Reach to your CI/CD pipelines.
3029

31-
You can ask about just one specific port.
30+
## Basic Usage
3231

33-
```ShellSession
34-
$ reach "web-server" "db-server" --port 1433
35-
analysis scope: TCP 1433
36-
not reachable
32+
The values for `source` and `destination` should each uniquely identify an EC2 instance in your AWS account. You can use an **instance ID** or a **name tag**, and you can enter just the first few characters instead of the entire value, as long as what you've entered matches exactly one EC2 instance.
33+
34+
Some examples:
35+
36+
```Text
37+
$ reach i-0452993c7efa3a314 i-02b8dfb5537e80860
3738
```
3839

39-
You can ask reach to explain its logic for its evaluation:
40+
```Text
41+
$ reach i-04 i-02
42+
```
4043

41-
```ShellSession
42-
$ reach "web-server" "db-server" --port 1433 --explain
43-
not reachable
44+
```Text
45+
$ reach web-instance database-instance
46+
```
4447

45-
- The instance "db-server" doesn't have any security groups with an inbound rule that allows access on port TCP 1433.
46-
- The subnet "database-private-subnet" in which the instance "db-server" resides doesn't have any network ACL rules that allow inbound traffic on port TCP 1433.
48+
```Text
49+
$ reach web data
4750
```
4851

49-
## CLI Syntax
52+
**Note:** Right now, Reach can only analyze the path between two EC2 instances when the instances are **in the same subnet**. Adding support for multiple subnets is the top priority and is currently in development.
5053

51-
`reach "first-instance" ["second-instance"] [OPTIONS]`
54+
## Initial Setup
5255

53-
### Options
56+
If you've never used Reach before, download the latest version for your platform from the [Releases](https://github.com/luhring/reach/releases) page. (Alternatively, if you've installed the [Go tools](https://golang.org/dl/), you can clone this repository and build from source.)
5457

55-
`--port`, `-p` Restrict analysis to a specified TCP port.
58+
You need to run Reach from somewhere where you've saved AWS credentials for your AWS account. Reach follows the standard process for locating and using AWS credentials, similar to the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) tool and other AWS-capable tools (e.g. Terraform). If you're not sure how to set up AWS credentials, check out [AWS's documentation for setting up credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html).
5659

57-
`--assert-reachable` Exit non-zero if no traffic is allowed from source to destination (within analysis scope, if specified).
60+
Once you've set up AWS credentials, you'll need to make sure your IAM user or role has permission to access the necessary resources in your AWS account. Reach only ever needs **read-only** access, it never modifies any resources in your AWS account. Reach makes various requests to the AWS API to describe various network-related resources, such as EC2 instances, VPCs, subnets, security groups, etc.
5861

59-
`--assert-not-reachable` Exit non-zero if any traffic can reach destination from source (within analysis scope, if specified).
62+
## More Features
6063

61-
`--explain` Explain how the configuration was analyzed.
64+
### Assertions
6265

63-
### Specifying an instance
66+
If you deploy infrastructure via CI/CD pipelines, it can be helpful to validate the network design itself before running any tests that rely on a correct network configuration.
6467

65-
reach is able to handle various methods of specifying an EC2 instance.
68+
You can use assertion flags to ensure that your source **can** or **cannot** reach your destination.
6669

67-
- **By Name tag.** Most people assign a descriptive name to each of their EC2 instances via a "Name" [tag](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html). Example: `"my-database-server"`.
68-
- **By Instance ID.** This is a reliably unique identifier within the EC2 service, and it's assigned by AWS. Example: `"i-08a43985c56df54e4"`.
70+
If an assertion succeeds, Reach exits `0`. If an assertion fails, Reach exits `2`.
6971

70-
#### Notes about instance specification strings
72+
To confirm that the source **can** reach the destination:
7173

72-
1. **Quotes:** Use quotes to surround instance specification strings as necessary within your shell environment. Quotes never hurt, but sometimes they can be left off -- for example, when using an instance name tag that contains no spaces, only letters and hyphens.
74+
```Text
75+
$ reach web-server database-server --assert-reachable
76+
```
77+
78+
To confirm that the source **cannot** reach the destination:
7379

74-
1. **Shortened strings:** To make CLI text entry less tedious, reach only requires enough of an instance specification string to be unique within the current AWS account and region. For example, let's say you have three EC2 instances in a particular region in your AWS account, and these instances are named "web-01", "web-02", and "db-master". You could refer to the "db-master" instance by typing just `"db"`, since no other instance begins with the same text. This would let you type the command `reach db --inbound`, and reach would understand that you were asking about inbound access to the "db-master" instance. However, it would not be sufficient to use the string `"web"`, since multiple instances have name tags that begin with the text "web". The rules for shortened strings apply to both name tags and instance IDs.
80+
```Text
81+
$ reach some-server super-sensitive-server --assert-not-reachable
82+
```
7583

76-
## Benefits
84+
### Explanations
7785

78-
**Instant diagnosis.** Instantly pinpoint missing links in a network setup in AWS.
86+
Normally, Reach's output is very basic. It displays a simple list of zero or more kinds of network traffic that are allowed to flow from the source to the destination. However, the process Reach uses to perform its analysis is more complex.
7987

80-
**Learn about your network.** Gain better insight into currently allowed network flows, and learn how resource configuration affects larger picture.
88+
If you're troubleshooting a network problem in AWS, it's probably more helpful to see _"why"_ the analysis result is what it is.
8189

82-
**Stay secure.** Tighten security without worrying about impacting any required network flows.
90+
You can tell Reach to expose the reasoning behind the displayed result by using the `--explain` flag:
91+
92+
```Text
93+
$ reach web-instance db-instance --explain
94+
```
8395

84-
**Better deployment pipelines.** Add into CI/CD pipelines alongside infrastructure as code (IaC) deployments to assert business expectations for your network, so you can confirm (or rule out) network-level problems before running integration or end-to-end tests.
96+
In this case, Reach will provide significantly more detail about the analysis. Specificially, the output will also show you:
8597

86-
## Road map
98+
- Exactly which "network points" were used in the analysis (not just the EC2 instance, but the EC2 instance's specific network interface, and the specific IP address attached to the network interface)
99+
- All of the "factors" (relevant aspects of your configuration) Reach used to figure out what traffic is being allowed by specific properties of your resources (e.g. security group rules, instance state, etc.)
87100

88-
- ~~Analyze traffic allowed from one EC2 instance to another, within the same subnet~~
89-
- Analyze traffic allowed from an EC2 instance in one subnet to an EC2 instance in another subnet, within the same VPC **<-- MVP**
90-
- Analyze traffic allowed between an EC2 instance and a specified IP address (e.g. user's IP address, specified hostname, etc.)
91-
- Support for non-EC2 resources within AWS (e.g. ELB, Lambda, gateways, etc.)
92-
- Support for VPC peering
101+
## Feature Ideas
102+
103+
- ~~**Same-subnet analysis:** Between two EC2 instances within the same subnet~~ (done!)
104+
- **Same-VPC analysis:** Between two EC2 instances within the same VPC, including for EC2 instances in separate subnets
105+
- **IP address analysis:** Between an EC2 instance and a specified IP address that may be outside of AWS entirely (enhancement idea: provide shortcuts for things like the user's own IP address, a specified hostname's resolved IP address, etc.)
106+
- **Filtered analysis:** Specify a particular kind of network traffic to analyze (e.g. a single TCP port) and return results only for that filter
107+
- **Other AWS resources:** Analyze other kinds of AWS resources than just EC2 instances (e.g. ELB, Lambda, VPC endpoints, etc.)
108+
- **Peered VPC analysis**: Between resources from separate but peered VPCs
93109
- Other things! Your ideas are welcome!
110+
111+
## Disclaimers
112+
113+
- This tool is a work in progress! Use at your own risk, and please submit issues as you encounter bugs or have feature requests.
114+
115+
- Because Reach gets all of its information from the AWS API, Reach makes no guarantees about network service accessibility with respect to the operating system or applications running on a host within the cloud environment. In other words, Reach can tell you if your VPC resources and EC2 instances are configured correctly, but Reach _cannot_ tell you if an OS firewall is blocking network traffic, or if an application listening on a port has crashed.

cmd/exit.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@ import (
77
)
88

99
func exitWithError(err error) {
10-
fmt.Println(err)
10+
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
1111

1212
os.Exit(1)
1313
}
1414

1515
func exitWithFailedAssertion(text string) {
1616
failedMessage := ansi.Color("assertion failed:", "red+b")
1717
secondaryMessage := ansi.Color(text, "red")
18-
fmt.Printf("%v %v\n", failedMessage, secondaryMessage)
18+
_, _ = fmt.Fprintf(os.Stderr, "\n%v %v\n", failedMessage, secondaryMessage)
1919

2020
os.Exit(2)
2121
}
2222

2323
func exitWithSuccessfulAssertion(text string) {
2424
succeededMessage := ansi.Color("assertion succeeded:", "green+b")
2525
secondaryMessage := ansi.Color(text, "green")
26-
fmt.Printf("%v %v\n", succeededMessage, secondaryMessage)
26+
_, _ = fmt.Fprintf(os.Stderr, "\n%v %v\n", succeededMessage, secondaryMessage)
2727

2828
os.Exit(0)
2929
}

cmd/root.go

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@ package cmd
33
import (
44
"errors"
55
"fmt"
6-
"github.com/luhring/reach/reach"
6+
"os"
7+
"strings"
8+
79
"github.com/spf13/cobra"
10+
11+
"github.com/luhring/reach/reach/analyzer"
12+
"github.com/luhring/reach/reach/aws"
13+
"github.com/luhring/reach/reach/aws/api"
14+
"github.com/luhring/reach/reach/explainer"
815
)
916

1017
const explainFlag = "explain"
11-
const portFlag = "port"
12-
const portFlagShorthand = "p"
18+
const vectorsFlag = "vectors"
1319
const assertReachableFlag = "assert-reachable"
1420
const assertNotReachableFlag = "assert-not-reachable"
1521

16-
var shouldExplain bool
17-
var port uint16
22+
var explain bool
23+
var showVectors bool
1824
var assertReachable bool
1925
var assertNotReachable bool
2026

@@ -35,42 +41,79 @@ See https://github.com/luhring/reach for documentation.`,
3541
return nil
3642
},
3743
Run: func(cmd *cobra.Command, args []string) {
38-
awsManager := reach.NewAWSManager()
44+
sourceIdentifier := args[0]
45+
destinationIdentifier := args[1]
3946

40-
instanceVector, err := awsManager.CreateInstanceVector(args[0], args[1])
47+
var provider aws.ResourceProvider = api.NewResourceProvider()
48+
49+
source, err := aws.NewSubject(sourceIdentifier, provider)
4150
if err != nil {
4251
exitWithError(err)
4352
}
53+
source.SetRoleToSource()
4454

45-
var filter *reach.TrafficAllowance
46-
if port == 0 {
47-
filter = nil
48-
} else {
49-
filter = reach.NewTrafficAllowanceForTCPPort(port)
50-
fmt.Printf("analysis scope: TCP %v\n", port)
55+
destination, err := aws.NewSubject(destinationIdentifier, provider)
56+
if err != nil {
57+
exitWithError(err)
58+
}
59+
destination.SetRoleToDestination()
60+
61+
if !explain && !showVectors {
62+
fmt.Printf("source: %s\ndestination: %s\n\n", source.ID, destination.ID)
63+
}
64+
65+
a := analyzer.New()
66+
analysis, err := a.Analyze(source, destination)
67+
if err != nil {
68+
exitWithError(err)
69+
}
70+
71+
mergedTraffic, err := analysis.MergedTraffic()
72+
if err != nil {
73+
exitWithError(err)
5174
}
5275

53-
analysis := instanceVector.Analyze(filter)
54-
fmt.Print(analysis.Results())
76+
if explain {
77+
ex := explainer.New(*analysis)
78+
fmt.Print(ex.Explain())
79+
} else if showVectors {
80+
var vectorOutputs []string
81+
82+
for _, v := range analysis.NetworkVectors {
83+
output := ""
84+
output += v.String()
5585

56-
if shouldExplain {
57-
fmt.Println("")
58-
fmt.Print(analysis.Explanation())
86+
vectorOutputs = append(vectorOutputs, output)
87+
}
88+
89+
fmt.Print(strings.Join(vectorOutputs, "\n"))
90+
} else {
91+
fmt.Print("network traffic allowed from source to destination:" + "\n")
92+
fmt.Print(mergedTraffic.ColorStringWithSymbols())
93+
94+
if len(analysis.NetworkVectors) > 1 {
95+
printMergedResultsWarning()
96+
}
5997
}
6098

99+
// fmt.Println(analysis.ToJSON()) // for debugging
100+
101+
const canReach = "source is able to reach destination"
102+
const cannotReach = "source is unable to reach destination"
103+
61104
if assertReachable {
62-
if analysis.PassesAssertReachable() {
63-
exitWithSuccessfulAssertion("specified traffic flow is allowed")
105+
if mergedTraffic.None() {
106+
exitWithFailedAssertion(cannotReach)
64107
} else {
65-
exitWithFailedAssertion("specified traffic flow is not allowed")
108+
exitWithSuccessfulAssertion(canReach)
66109
}
67110
}
68111

69112
if assertNotReachable {
70-
if analysis.PassesAssertNotReachable() {
71-
exitWithSuccessfulAssertion("none of specified traffic flow is allowed")
113+
if mergedTraffic.None() {
114+
exitWithSuccessfulAssertion(cannotReach)
72115
} else {
73-
exitWithFailedAssertion("some or all of specified traffic flow is allowed")
116+
exitWithFailedAssertion(canReach)
74117
}
75118
}
76119
},
@@ -83,8 +126,13 @@ func Execute() {
83126
}
84127

85128
func init() {
86-
rootCmd.Flags().BoolVar(&shouldExplain, explainFlag, false, "explain how the configuration was analyzed")
87-
rootCmd.Flags().Uint16VarP(&port, portFlag, portFlagShorthand, 0, "restrict analysis to a specified TCP port")
88-
rootCmd.Flags().BoolVar(&assertReachable, assertReachableFlag, false, "exit non-zero if no traffic is allowed from source to destination (within analysis scope, if specified)")
89-
rootCmd.Flags().BoolVar(&assertNotReachable, assertNotReachableFlag, false, "exit non-zero if any traffic can reach destination from source (within analysis scope, if specified)")
129+
rootCmd.Flags().BoolVar(&explain, explainFlag, false, "explain how the configuration was analyzed")
130+
rootCmd.Flags().BoolVar(&showVectors, vectorsFlag, false, "show allowed traffic in terms of network vectors")
131+
rootCmd.Flags().BoolVar(&assertReachable, assertReachableFlag, false, "exit non-zero if no traffic is allowed from source to destination")
132+
rootCmd.Flags().BoolVar(&assertNotReachable, assertNotReachableFlag, false, "exit non-zero if any traffic can reach destination from source")
133+
}
134+
135+
func printMergedResultsWarning() {
136+
const mergedResultsWarning = "IMPORTANT: Reach detected more than one network path between the source and destination. Reach calls these paths \"network vectors\". The analysis result shown above is the merging of all network vectors' analysis results. The impact that infrastructure configuration has on actual network reachability might vary based on the way hosts are configured to use their network interfaces, and Reach is unable to access any configuration internal to a host. To see the network reachability across individual network vectors, run the command again with '--vectors'.\n\n"
137+
_, _ = fmt.Fprint(os.Stderr, "\n"+mergedResultsWarning)
90138
}

0 commit comments

Comments
 (0)