├── awsdial └── dialer.go ├── cleanup.go ├── cleanup └── cleanup.go ├── docs ├── README.md └── extcap-opts.png ├── go.mod ├── go.sum ├── launch-template.yml ├── remote └── remote.go ├── vpcshark.go └── wireshark.go /awsdial/dialer.go: -------------------------------------------------------------------------------- 1 | package awsdial 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/ssm" 9 | "github.com/aws/session-manager-plugin/src/datachannel" 10 | "github.com/aws/session-manager-plugin/src/log" 11 | "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session" 12 | _ "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session/portsession" 13 | "github.com/google/uuid" 14 | "net" 15 | "os" 16 | "regexp" 17 | "strconv" 18 | "sync" 19 | ) 20 | 21 | type Dialer struct { 22 | Client *ssm.Client 23 | Region string 24 | localPort int 25 | mut sync.Mutex 26 | } 27 | 28 | func (d *Dialer) Dial(ctx context.Context, target string, port int) (net.Conn, error) { 29 | d.mut.Lock() 30 | defer d.mut.Unlock() 31 | 32 | if d.localPort <= 0 { 33 | err := d.dial(ctx, target, port) 34 | if err != nil { 35 | return nil, fmt.Errorf(": %w", err) 36 | } 37 | } 38 | 39 | return net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", d.localPort)) 40 | } 41 | 42 | func (d *Dialer) dial(ctx context.Context, target string, port int) error { 43 | in := &ssm.StartSessionInput{ 44 | DocumentName: aws.String("AWS-StartPortForwardingSession"), 45 | Target: aws.String(target), 46 | Parameters: map[string][]string{ 47 | "portNumber": {strconv.Itoa(port)}, 48 | }, 49 | } 50 | 51 | start, err := d.Client.StartSession(ctx, in) 52 | if err != nil { 53 | return fmt.Errorf("calling StartSession API: %w", err) 54 | } 55 | 56 | ep, err := ssm.NewDefaultEndpointResolver().ResolveEndpoint(d.Region, ssm.EndpointResolverOptions{}) 57 | if err != nil { 58 | return fmt.Errorf(": %w", err) 59 | } 60 | 61 | ssmSession := &session.Session{ 62 | DataChannel: &datachannel.DataChannel{}, 63 | SessionId: *start.SessionId, 64 | StreamUrl: *start.StreamUrl, 65 | TokenValue: *start.TokenValue, 66 | Endpoint: ep.URL, 67 | ClientId: uuid.NewString(), 68 | TargetId: target, 69 | } 70 | 71 | r, newStdout, err := os.Pipe() 72 | if err != nil { 73 | return fmt.Errorf(": %w", err) 74 | } 75 | 76 | oldStdout := os.Stdout 77 | os.Stdout = newStdout 78 | defer func() { 79 | os.Stdout = oldStdout 80 | }() 81 | 82 | go func() { 83 | err = ssmSession.Execute(log.Logger(false, ssmSession.ClientId)) 84 | if err != nil { 85 | panic(fmt.Sprintf("%+v", err)) 86 | } 87 | }() 88 | 89 | scan := bufio.NewScanner(r) 90 | re := regexp.MustCompile(`Port (\d+) opened for sessionId ([^.]+).`) 91 | 92 | // TODO: we probably need to keep reading this rather than bailing after getting port 93 | for scan.Scan() { 94 | matches := re.FindStringSubmatch(scan.Text()) 95 | if len(matches) == 0 { 96 | continue 97 | } 98 | 99 | d.localPort, _ = strconv.Atoi(matches[1]) 100 | break 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /cleanup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aidansteele/vpcshark/cleanup" 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/config" 9 | "github.com/aws/aws-sdk-go-v2/service/ec2" 10 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 11 | ) 12 | 13 | func cleanupAll(ctx context.Context, profile, region string) error { 14 | cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(profile), config.WithRegion(region)) 15 | if err != nil { 16 | return fmt.Errorf(": %w", err) 17 | } 18 | 19 | api := ec2.NewFromConfig(cfg) 20 | 21 | err = cleanup.TerminateInstances(ctx, api, []types.Filter{ 22 | { 23 | Name: aws.String("tag-key"), 24 | Values: []string{"vpcshark"}, 25 | }, 26 | { 27 | Name: aws.String("instance-state-name"), 28 | Values: []string{"running"}, 29 | }, 30 | }) 31 | if err != nil { 32 | return fmt.Errorf(": %w", err) 33 | } 34 | 35 | filters := []types.Filter{ 36 | { 37 | Name: aws.String("description"), 38 | Values: []string{trafficMirrorDescription}, 39 | }, 40 | } 41 | 42 | _, err = cleanup.DeleteSessions(ctx, api, filters) 43 | if err != nil { 44 | return fmt.Errorf(": %w", err) 45 | } 46 | 47 | err = cleanup.DeleteTargets(ctx, api, filters) 48 | if err != nil { 49 | return fmt.Errorf(": %w", err) 50 | } 51 | 52 | err = cleanup.DeleteFilters(ctx, api, filters) 53 | if err != nil { 54 | return fmt.Errorf(": %w", err) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /cleanup/cleanup.go: -------------------------------------------------------------------------------- 1 | package cleanup 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aws/aws-sdk-go-v2/service/ec2" 7 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 8 | ) 9 | 10 | func TerminateInstances(ctx context.Context, api *ec2.Client, filters []types.Filter) error { 11 | ip := ec2.NewDescribeInstancesPaginator(api, &ec2.DescribeInstancesInput{Filters: filters}) 12 | for ip.HasMorePages() { 13 | page, err := ip.NextPage(ctx) 14 | if err != nil { 15 | return fmt.Errorf(": %w", err) 16 | } 17 | 18 | for _, reservation := range page.Reservations { 19 | for _, instance := range reservation.Instances { 20 | instanceId := instance.InstanceId 21 | fmt.Printf("Terminating %s\n", *instanceId) 22 | _, err = api.TerminateInstances(ctx, &ec2.TerminateInstancesInput{InstanceIds: []string{*instanceId}}) 23 | if err != nil { 24 | return fmt.Errorf(": %w", err) 25 | } 26 | } 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func DeleteSessions(ctx context.Context, api *ec2.Client, filters []types.Filter) ([]types.TrafficMirrorSession, error) { 34 | sessions := []types.TrafficMirrorSession{} 35 | 36 | sp := ec2.NewDescribeTrafficMirrorSessionsPaginator(api, &ec2.DescribeTrafficMirrorSessionsInput{Filters: filters}) 37 | for sp.HasMorePages() { 38 | page, err := sp.NextPage(ctx) 39 | if err != nil { 40 | return nil, fmt.Errorf(": %w", err) 41 | } 42 | 43 | for _, session := range page.TrafficMirrorSessions { 44 | session := session 45 | sessions = append(sessions, session) 46 | sessionId := session.TrafficMirrorSessionId 47 | fmt.Printf("Deleting %s\n", *sessionId) 48 | _, err = api.DeleteTrafficMirrorSession(ctx, &ec2.DeleteTrafficMirrorSessionInput{TrafficMirrorSessionId: sessionId}) 49 | if err != nil { 50 | return nil, fmt.Errorf(": %w", err) 51 | } 52 | } 53 | } 54 | 55 | return sessions, nil 56 | } 57 | 58 | func DeleteFilters(ctx context.Context, api *ec2.Client, filters []types.Filter) error { 59 | fp := ec2.NewDescribeTrafficMirrorFiltersPaginator(api, &ec2.DescribeTrafficMirrorFiltersInput{Filters: filters}) 60 | for fp.HasMorePages() { 61 | page, err := fp.NextPage(ctx) 62 | if err != nil { 63 | return fmt.Errorf(": %w", err) 64 | } 65 | 66 | for _, filter := range page.TrafficMirrorFilters { 67 | filterId := filter.TrafficMirrorFilterId 68 | fmt.Printf("Deleting %s\n", *filterId) 69 | _, err = api.DeleteTrafficMirrorFilter(ctx, &ec2.DeleteTrafficMirrorFilterInput{TrafficMirrorFilterId: filterId}) 70 | if err != nil { 71 | return fmt.Errorf(": %w", err) 72 | } 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func DeleteTargets(ctx context.Context, api *ec2.Client, filters []types.Filter) error { 80 | tp := ec2.NewDescribeTrafficMirrorTargetsPaginator(api, &ec2.DescribeTrafficMirrorTargetsInput{Filters: filters}) 81 | for tp.HasMorePages() { 82 | page, err := tp.NextPage(ctx) 83 | if err != nil { 84 | return fmt.Errorf(": %w", err) 85 | } 86 | 87 | for _, target := range page.TrafficMirrorTargets { 88 | targetId := target.TrafficMirrorTargetId 89 | fmt.Printf("Deleting %s\n", *targetId) 90 | _, err = api.DeleteTrafficMirrorTarget(ctx, &ec2.DeleteTrafficMirrorTargetInput{TrafficMirrorTargetId: targetId}) 91 | if err != nil { 92 | return fmt.Errorf(": %w", err) 93 | } 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## 2024 Update 2 | 3 | All the code in this repo was written in mid 2022 when VPC traffic mirroring was 4 | relatively new and shiny. I had kept this code closed-source because I had unrealistic 5 | ambitions about perhaps making this project open-core with a value-add SaaS on the 6 | side. In the interim, it seems that AWS has launched sixth, seventh and eighth-generation 7 | EC2 instance family types [without][docs] traffic mirroring support. I have no inside 8 | knowledge, but that indicates to me that the functionality will only become less useful 9 | as the fifth-generation EC2 instances become increasingly obsolete. So here's an open 10 | source dump of code for historical interest. 11 | 12 | [docs]: https://docs.aws.amazon.com/vpc/latest/mirroring/traffic-mirroring-network-limitations.html 13 | 14 | # vpcshark 15 | 16 | `vpcshark` is an `extcap` Wireshark plugin that automates VPC traffic mirroring. 17 | Specifically it creates (on-demand, for each Wireshark session): 18 | 19 | * An EC2 instance to receive mirrored traffic 20 | * A traffic mirror _target_ pointed at the above EC2 instance 21 | * A traffic mirror _filter_ that permits all traffic (TODO: scope down to match 22 | Wireshark capture filter) 23 | * A traffic mirror _session_ for the EC2 ENI that you are mirroring 24 | 25 | It _should_ terminate the EC2 instance after the Wireshark capture session 26 | has been stopped. But it is buggy and not properly implemented yet. Likewise, 27 | automatic destruction of traffic mirror sessions is not implemented. Run 28 | `vpcshark --cleanup --profile ` to clean up traffic mirror 29 | resources. 30 | 31 | ## Usage 32 | 33 | First you need to build the remote tool that is copied to and executed on the 34 | remote EC2 instance. You can do that like this: 35 | 36 | (cd remote; GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w"; upx remote) 37 | 38 | The `upx` step is optional, but makes copying the file faster. After that, you 39 | can `go build` the vpcshark executable like normal. These steps are needed 40 | because the `remote` binary is actually copied into the `vpcshark` binary. 41 | 42 | Logs will appear in `/tmp/vpcshark.log`. 43 | 44 | You need to copy (or symlink) `vpcshark` into the `extcap` directory. By default 45 | that is `/Applications/Wireshark.app/Contents/MacOS/extcap/` on macOS. When you 46 | launch Wireshark, you will see a new capture interface named _AWS VPC Traffic Mirroring: awsvpc_. 47 | Double-clicking this will yield the following dialogue box: 48 | 49 | ![extcap options](extcap-opts.png) 50 | 51 | First, select an AWS profile. If you use AWS SSO, then you should have already 52 | previously logged into this profile recently. 53 | 54 | Next, click _Load VPCs…_. This will refresh the VPC dropdown box. Select the VPC 55 | that contains the ENI you are interested in mirroring. 56 | 57 | Next, click _Load ENIs…_. This will refresh the ENI dropbox box. Select the ENI 58 | you are interested in mirroring. 59 | 60 | Finally, click _Load templates…_ and select the EC2 launch template you want to 61 | use for the temporary EC2 instance that will be launched as the traffic mirror 62 | target. 63 | 64 | **Note**: the launch template should include everything that is required to launch 65 | an EC2 instance at a minimum. This means you need to specify: 66 | 67 | * An AMI. This should be one based on Amazon Linux 2. Specifically one that can 68 | `sudo yum install socat`. 69 | * An instance type 70 | * A subnet (in the same VPC that you are mirroring traffic) 71 | * A security group that has at least UDP port 4789 open to the VPC CIDR and 72 | SSH open to your personal IP address. 73 | * An ENI with an auto-assigned public IP address (if that is not already the 74 | default for your chosen subnet) 75 | -------------------------------------------------------------------------------- /docs/extcap-opts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/vpcshark/cbc4e2c09e38480e70b0f8bf6dc7c096701dac93/docs/extcap-opts.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aidansteele/vpcshark 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.16.16 7 | github.com/aws/aws-sdk-go-v2/config v1.17.8 8 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.1 9 | github.com/aws/aws-sdk-go-v2/service/ssm v1.31.0 10 | github.com/aws/session-manager-plugin v0.0.0-20221012155945-c523002ee02c 11 | github.com/davecgh/go-spew v1.1.1 12 | github.com/google/gopacket v1.1.19 13 | github.com/google/uuid v1.3.0 14 | github.com/mmmorris1975/ssm-session-client v0.204.0 15 | github.com/spf13/cobra v1.6.0 16 | golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 17 | golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 18 | gopkg.in/ini.v1 v1.67.0 19 | ) 20 | 21 | require ( 22 | github.com/aws/aws-sdk-go v1.44.76 // indirect 23 | github.com/aws/aws-sdk-go-v2/credentials v1.12.21 // indirect 24 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect 32 | github.com/aws/smithy-go v1.13.3 // indirect 33 | github.com/bramvdbogaerde/go-scp v1.2.1-0.20220904182223-71df80a4808a // indirect 34 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect 35 | github.com/fsnotify/fsnotify v1.5.4 // indirect 36 | github.com/gorilla/websocket v1.5.0 // indirect 37 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 38 | github.com/jmespath/go-jmespath v0.4.0 // indirect 39 | github.com/pmezard/go-difflib v1.0.0 // indirect 40 | github.com/spf13/pflag v1.0.5 // indirect 41 | github.com/stretchr/objx v0.4.0 // indirect 42 | github.com/stretchr/testify v1.8.0 // indirect 43 | github.com/twinj/uuid v0.0.0-20151029044442-89173bcdda19 // indirect 44 | github.com/xtaci/smux v1.5.16 // indirect 45 | golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect 46 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | 50 | replace github.com/mmmorris1975/ssm-session-client => github.com/aidansteele/ssm-session-client v0.0.0-20221014002521-e2f645594da8 51 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aidansteele/ssm-session-client v0.0.0-20221014002521-e2f645594da8 h1:vvqQdLpoaA/jaIKzu/1CspBmZqOQDMUTwi6NHRD4lpo= 2 | github.com/aidansteele/ssm-session-client v0.0.0-20221014002521-e2f645594da8/go.mod h1:0YtXtrJsDoL541+CYcQRUfFfI3TpsriNpWr6ZTVPATw= 3 | github.com/aws/aws-sdk-go v1.44.76 h1:5e8yGO/XeNYKckOjpBKUd5wStf0So3CrQIiOMCVLpOI= 4 | github.com/aws/aws-sdk-go v1.44.76/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 5 | github.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo= 6 | github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk= 7 | github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= 8 | github.com/aws/aws-sdk-go-v2/config v1.16.1/go.mod h1:4SKzBMiB8lV0fw2w7eDBo/LjQyHFITN4vUUuqpurFmI= 9 | github.com/aws/aws-sdk-go-v2/config v1.17.8 h1:b9LGqNnOdg9vR4Q43tBTVWk4J6F+W774MSchvKJsqnE= 10 | github.com/aws/aws-sdk-go-v2/config v1.17.8/go.mod h1:UkCI3kb0sCdvtjiXYiU4Zx5h07BOpgBTtkPu/49r+kA= 11 | github.com/aws/aws-sdk-go-v2/credentials v1.12.13/go.mod h1:9fDEemXizwXrxPU1MTzv69LP/9D8HVl5qHAQO9A9ikY= 12 | github.com/aws/aws-sdk-go-v2/credentials v1.12.21 h1:4tjlyCD0hRGNQivh5dN8hbP30qQhMLBE/FgQR1vHHWM= 13 | github.com/aws/aws-sdk-go-v2/credentials v1.12.21/go.mod h1:O+4XyAt4e+oBAoIwNUYkRg3CVMscaIJdmZBOcPgJ8D8= 14 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12/go.mod h1:aZ4vZnyUuxedC7eD4JyEHpGnCz+O2sHQEx3VvAwklSE= 15 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 h1:r08j4sbZu/RVi+BNxkBJwPMUYY3P8mgSDuKkZ/ZN1lE= 16 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17/go.mod h1:yIkQcCDYNsZfXpd5UX2Cy+sWA1jPgIhGTw9cOBzfVnQ= 17 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ= 18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 h1:s4g/wnzMf+qepSNgTvaQQHNxyMLKSawNhKCPNy++2xY= 19 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= 20 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12/go.mod h1:ckaCVTEdGAxO6KwTGzgskxR1xM+iJW4lxMyDFVda2Fc= 21 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 h1:/K482T5A3623WJgWT8w1yRAFK4RzGzEl7y39yhtn9eA= 22 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= 23 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19/go.mod h1:cVHo8KTuHjShb9V8/VjH3S/8+xPu16qx8fdGwmotJhE= 24 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 h1:wj5Rwc05hvUSvKuOF29IYb9QrCLjU+rHAy/x/o0DK2c= 25 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24/go.mod h1:jULHjqqjDlbyTa7pfM7WICATnOv+iOhjletM3N0Xbu8= 26 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.52.1/go.mod h1:YbPg6ou7dlvFTJMmbV3zhec+A22S1Ow+ZB6k6xUs9oY= 27 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.1 h1:jSS5gynKz4XaGcs6m25idCTN+tvPkRJ2WedSWCcZEjI= 28 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.1/go.mod h1:0+6fPoY0SglgzQUs2yml7X/fup12cMlVumJufh5npRQ= 29 | github.com/aws/aws-sdk-go-v2/service/ec2instanceconnect v1.14.4/go.mod h1:hExFZoQ1a0L1uOEOtlGJLb4h0Tx6iSxnygSJ65zVito= 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12/go.mod h1:1TODGhheLWjpQWSuhYuAUWYTCKwEjx2iblIFKDHjeTc= 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 h1:Jrd/oMh0PKQc6+BowB+pLEwLIgaQF29eYbe7E1Av9Ug= 32 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= 33 | github.com/aws/aws-sdk-go-v2/service/ssm v1.27.9/go.mod h1:tHC1rUMDPt7ABC+ne8/jyzQ91rGqUFpvV08HUJmydWo= 34 | github.com/aws/aws-sdk-go-v2/service/ssm v1.31.0 h1:zBiXS2v+ycKZ61bTBR1jGqIJhEW7Qjcl8c/mrkUNeog= 35 | github.com/aws/aws-sdk-go-v2/service/ssm v1.31.0/go.mod h1:JtkQSJFGEovwP6s+guH5Ap7iUemh3nMqHtg5liCv9ok= 36 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.16/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY= 37 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 h1:pwvCchFUEnlceKIgPUouBJwK81aCkQ8UDMORfeFtW10= 38 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.23/go.mod h1:/w0eg9IhFGjGyyncHIQrXtU8wvNsTJOP0R6PPj0wf80= 39 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 h1:OwhhKc1P9ElfWbMKPIbMMZBV6hzJlL2JKD76wNNVzgQ= 40 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6/go.mod h1:csZuQY65DAdFBt1oIjO5hhBR49kQqop4+lcuCjf2arA= 41 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0= 42 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 h1:9pPi0PsFNAGILFfPCk8Y0iyEBGc6lu6OQ97U7hmdesg= 43 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.19/go.mod h1:h4J3oPZQbxLhzGnk+j9dfYHi5qIOVJ5kczZd658/ydM= 44 | github.com/aws/session-manager-plugin v0.0.0-20221012155945-c523002ee02c h1:6cCrrTmS+7B+saEBhMnNblArJpA7BNmjd9F6MUHS6sQ= 45 | github.com/aws/session-manager-plugin v0.0.0-20221012155945-c523002ee02c/go.mod h1:7n17tunRPUsniNBu5Ja9C7WwJWTdOzaLqr/H0Ns3uuI= 46 | github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 47 | github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA= 48 | github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 49 | github.com/bramvdbogaerde/go-scp v1.2.1-0.20220904182223-71df80a4808a h1:iFgMmu2vibVPdV3orKTrEvxmsJAlgKzpQE/PwPREZe4= 50 | github.com/bramvdbogaerde/go-scp v1.2.1-0.20220904182223-71df80a4808a/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yPXQTPshACY8vQ0= 51 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= 52 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= 53 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 54 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 56 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= 58 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 59 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 60 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 61 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 62 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 63 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 64 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 65 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 66 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 67 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 68 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 69 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 70 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 71 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 72 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 73 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 74 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 75 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 76 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 79 | github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= 80 | github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 81 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 82 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 85 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 86 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 87 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 88 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 89 | github.com/twinj/uuid v0.0.0-20151029044442-89173bcdda19 h1:HlxV0XiEKMMyjS3gGtJmmFZsxQ22GsLvA7F980il+1w= 90 | github.com/twinj/uuid v0.0.0-20151029044442-89173bcdda19/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= 91 | github.com/xtaci/smux v1.5.16 h1:FBPYOkW8ZTjLKUM4LI4xnnuuDC8CQ/dB04HD519WoEk= 92 | github.com/xtaci/smux v1.5.16/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= 93 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 94 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 95 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 96 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 97 | golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 h1:x8vtB3zMecnlqZIwJNUUpwYKYSqCz5jXbiyv0ZJJZeI= 98 | golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 99 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 100 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 101 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 103 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 104 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 105 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 106 | golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E= 107 | golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 108 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY= 111 | golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= 123 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 125 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 126 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 127 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 128 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 129 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 130 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 131 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 132 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 133 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 137 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 138 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 139 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 140 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 141 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 142 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | -------------------------------------------------------------------------------- /launch-template.yml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | ImageId: 3 | Type: AWS::SSM::Parameter::Value 4 | Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-arm64-gp2 5 | SubnetId: 6 | Type: AWS::EC2::Subnet::Id 7 | VpcId: 8 | Type: AWS::EC2::VPC::Id 9 | SshIpAddress: 10 | Type: String 11 | 12 | Resources: 13 | LaunchTemplate: 14 | Type: AWS::EC2::LaunchTemplate 15 | Properties: 16 | LaunchTemplateName: vpcshark-public 17 | LaunchTemplateData: 18 | IamInstanceProfile: 19 | Arn: !GetAtt InstanceProfile.Arn 20 | ImageId: !Ref ImageId 21 | InstanceType: t4g.nano 22 | InstanceInitiatedShutdownBehavior: terminate 23 | NetworkInterfaces: 24 | - DeviceIndex: 0 25 | SubnetId: !Ref SubnetId 26 | Groups: [ !Ref SecurityGroup ] 27 | AssociatePublicIpAddress: true 28 | TagSpecifications: 29 | - ResourceType: instance 30 | Tags: 31 | - Key: Name 32 | Value: vpcshark 33 | - Key: vpcshark 34 | Value: "" 35 | - ResourceType: network-interface 36 | Tags: 37 | - Key: Name 38 | Value: vpcshark 39 | - Key: vpcshark 40 | Value: "" 41 | 42 | InstanceRole: 43 | Type: AWS::IAM::Role 44 | Properties: 45 | AssumeRolePolicyDocument: 46 | Statement: 47 | - Effect: Allow 48 | Action: sts:AssumeRole 49 | Principal: 50 | Service: ec2.amazonaws.com 51 | ManagedPolicyArns: 52 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 53 | Policies: 54 | - PolicyName: AllowTrafficMirrorControl 55 | PolicyDocument: 56 | Version: "2012-10-17" 57 | Statement: 58 | - Effect: Allow 59 | Action: ec2:DescribeTrafficMirror* 60 | Resource: "*" 61 | - Effect: Allow 62 | Action: ec2:DeleteTrafficMirror* 63 | Resource: "*" 64 | Condition: 65 | "Null": 66 | aws:ResourceTag/vpcshark: false 67 | - Effect: Allow 68 | Action: ec2:CreateTags 69 | Resource: !Sub arn:aws:ec2:*:${AWS::AccountId}:traffic-mirror-*/* 70 | Tags: 71 | - Key: Name 72 | Value: vpcshark 73 | - Key: vpcshark 74 | Value: "" 75 | 76 | InstanceProfile: 77 | Type: AWS::IAM::InstanceProfile 78 | Properties: 79 | Roles: [ !Ref InstanceRole ] 80 | 81 | SecurityGroup: 82 | Type: AWS::EC2::SecurityGroup 83 | Properties: 84 | GroupDescription: Allow SSH and VXLAN 85 | VpcId: !Ref VpcId 86 | SecurityGroupIngress: 87 | - CidrIp: !Sub "${SshIpAddress}/32" 88 | FromPort: 22 89 | ToPort: 22 90 | IpProtocol: tcp 91 | - CidrIp: "0.0.0.0/0" 92 | FromPort: 4789 93 | ToPort: 4789 94 | IpProtocol: udp 95 | Tags: 96 | - Key: Name 97 | Value: vpcshark 98 | - Key: vpcshark 99 | Value: "" 100 | 101 | Outputs: 102 | LaunchTemplate: 103 | Value: !Ref LaunchTemplate 104 | -------------------------------------------------------------------------------- /remote/remote.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "encoding/binary" 8 | "fmt" 9 | "github.com/aidansteele/vpcshark/cleanup" 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/service/ec2" 13 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 14 | "golang.org/x/sys/unix" 15 | "net" 16 | "os" 17 | "os/exec" 18 | "os/signal" 19 | "time" 20 | ) 21 | 22 | func main() { 23 | daemonizeIfParent() 24 | 25 | ctx := context.Background() 26 | sigctx, done := signal.NotifyContext(ctx, os.Interrupt, os.Kill, unix.SIGHUP) 27 | defer done() 28 | 29 | f, err := os.OpenFile("/tmp/vpcshark.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 30 | if err != nil { 31 | panic(fmt.Sprintf("%+v", err)) 32 | } 33 | defer f.Close() 34 | 35 | fmt.Fprintf(f, "%s pre-packetForwardLoop\n", time.Now()) 36 | err = packetForwardLoop(sigctx) 37 | fmt.Fprintf(f, "%s got error %+v\n", time.Now(), err) 38 | 39 | awsRegion, targetId := os.Args[1], os.Args[2] 40 | fmt.Fprintf(f, "%s region=%s target=%s\n", time.Now(), awsRegion, targetId) 41 | 42 | fmt.Fprintf(f, "%s post-packetForwardLoop\n", time.Now()) 43 | fmt.Fprintf(f, "%s pre-cleanup\n", time.Now()) 44 | err = cleanupAfterMe(ctx, awsRegion, targetId) 45 | fmt.Fprintf(f, "%s got error %+v\n", time.Now(), err) 46 | fmt.Fprintf(f, "%s post-cleanup\n", time.Now()) 47 | 48 | cmd := exec.Command("sudo", "shutdown", "now") 49 | err = cmd.Run() 50 | if err != nil { 51 | panic(fmt.Sprintf("%+v", err)) 52 | } 53 | 54 | fmt.Fprintf(f, "%s post-shutdown (???)\n", time.Now()) 55 | } 56 | 57 | func daemonizeIfParent() { 58 | if _, isChild := os.LookupEnv("CHILD"); isChild { 59 | return 60 | } 61 | 62 | exe, err := os.Executable() 63 | if err != nil { 64 | panic(fmt.Sprintf("%+v", err)) 65 | } 66 | 67 | cmd := exec.Command(exe, os.Args[1:]...) 68 | cmd.Env = append(os.Environ(), "CHILD=true") 69 | err = cmd.Start() 70 | if err != nil { 71 | panic(fmt.Sprintf("%+v", err)) 72 | } 73 | 74 | os.Exit(0) 75 | } 76 | 77 | func cleanupAfterMe(ctx context.Context, awsRegion, targetId string) error { 78 | cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(awsRegion)) 79 | if err != nil { 80 | return fmt.Errorf(": %w", err) 81 | } 82 | 83 | api := ec2.NewFromConfig(cfg) 84 | sessions, err := cleanup.DeleteSessions(ctx, api, []types.Filter{ 85 | { 86 | Name: aws.String("traffic-mirror-target-id"), 87 | Values: []string{targetId}, 88 | }, 89 | }) 90 | if err != nil { 91 | return fmt.Errorf(": %w", err) 92 | } 93 | 94 | filterIds := map[string]struct{}{} 95 | for _, session := range sessions { 96 | filterIds[*session.TrafficMirrorFilterId] = struct{}{} 97 | } 98 | 99 | for filterId := range filterIds { 100 | _, err = api.DeleteTrafficMirrorFilter(ctx, &ec2.DeleteTrafficMirrorFilterInput{TrafficMirrorFilterId: &filterId}) 101 | if err != nil { 102 | return fmt.Errorf(": %w", err) 103 | } 104 | } 105 | 106 | _, err = api.DeleteTrafficMirrorTarget(ctx, &ec2.DeleteTrafficMirrorTargetInput{TrafficMirrorTargetId: &targetId}) 107 | if err != nil { 108 | return fmt.Errorf(": %w", err) 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func packetForwardLoop(ctx context.Context) error { 115 | clientListener, err := net.Listen("tcp", "127.0.0.1:4790") 116 | if err != nil { 117 | return fmt.Errorf(": %w", err) 118 | } 119 | 120 | go func() { 121 | <-ctx.Done() 122 | clientListener.Close() 123 | }() 124 | 125 | clientConn, err := clientListener.Accept() 126 | if err != nil { 127 | return fmt.Errorf(": %w", err) 128 | } 129 | 130 | vxlanConn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 4789}) 131 | if err != nil { 132 | return fmt.Errorf(": %w", err) 133 | } 134 | 135 | go func() { 136 | <-ctx.Done() 137 | vxlanConn.Close() 138 | }() 139 | 140 | buf := make([]byte, 9003) 141 | 142 | for { 143 | n, err := vxlanConn.Read(buf[2:]) 144 | if err != nil { 145 | return fmt.Errorf(": %w", err) 146 | } 147 | 148 | binary.BigEndian.PutUint16(buf, uint16(n)) 149 | _, err = clientConn.Write(buf[:n+2]) 150 | if err != nil { 151 | return fmt.Errorf(": %w", err) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /vpcshark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ed25519" 7 | "crypto/rand" 8 | _ "embed" 9 | "encoding/base64" 10 | "encoding/binary" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "github.com/aidansteele/vpcshark/awsdial" 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | "github.com/aws/aws-sdk-go-v2/config" 17 | "github.com/aws/aws-sdk-go-v2/service/ec2" 18 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 19 | "github.com/aws/aws-sdk-go-v2/service/ssm" 20 | ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types" 21 | "github.com/aws/aws-sdk-go/aws/endpoints" 22 | "github.com/bramvdbogaerde/go-scp" 23 | "github.com/google/gopacket" 24 | "github.com/google/gopacket/layers" 25 | "github.com/google/gopacket/pcapgo" 26 | "github.com/spf13/cobra" 27 | "golang.org/x/crypto/ssh" 28 | "golang.org/x/sync/errgroup" 29 | "gopkg.in/ini.v1" 30 | "io" 31 | mrand "math/rand" 32 | "net" 33 | "os" 34 | "sort" 35 | "strings" 36 | "syscall" 37 | "time" 38 | ) 39 | 40 | const trafficMirrorVNI = 0xBEEF 41 | const trafficMirrorDescription = "vpcshark" 42 | 43 | func main() { 44 | var err error 45 | os.Stderr, err = os.OpenFile("/tmp/vpcshark.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0777) 46 | if err != nil { 47 | panic(fmt.Sprintf("%+v", err)) 48 | } 49 | 50 | mrand.Seed(time.Now().UnixNano()) 51 | fmt.Fprintf(os.Stderr, "%x\n", mrand.Int63()) 52 | 53 | j, _ := json.MarshalIndent(os.Args, "", " ") 54 | fmt.Fprintln(os.Stderr, string(j)) 55 | 56 | rootCmd := &cobra.Command{RunE: runVpcshark} 57 | pf := rootCmd.PersistentFlags() 58 | pf.Float32("extcap-version", 0, "") 59 | pf.Bool("cleanup", false, "") 60 | pf.Bool("extcap-config", false, "") 61 | pf.Bool("extcap-interfaces", false, "") 62 | pf.Bool("extcap-dlts", false, "") 63 | pf.Bool("capture", false, "") 64 | pf.String("extcap-reload-option", "", "") 65 | pf.String("extcap-interface", "", "") 66 | pf.String("fifo", "", "") 67 | pf.String("extcap-control-out", "", "") 68 | pf.String("extcap-control-in", "", "") 69 | pf.String("profile", "", "") 70 | pf.String("region", "", "") 71 | pf.String("vpc", "", "") 72 | pf.String("eni", "", "") 73 | pf.String("launch-template-id", "", "") 74 | pf.String("connectivity", "", "") 75 | 76 | ctx := context.Background() 77 | err = rootCmd.ExecuteContext(ctx) 78 | if err != nil { 79 | panic(fmt.Sprintf("%+v", err)) 80 | } 81 | 82 | fmt.Fprintln(os.Stderr, "Exited cleanly") 83 | } 84 | 85 | func runVpcshark(cmd *cobra.Command, args []string) error { 86 | ctx := cmd.Context() 87 | 88 | pf := cmd.PersistentFlags() 89 | cleanup, _ := pf.GetBool("cleanup") 90 | extcapConfig, _ := pf.GetBool("extcap-config") 91 | extcapInterfaces, _ := pf.GetBool("extcap-interfaces") 92 | extcapDLTs, _ := pf.GetBool("extcap-dlts") 93 | capture, _ := pf.GetBool("capture") 94 | extcapReloadOption, _ := pf.GetString("extcap-reload-option") 95 | //extcapInterface, _ := pf.GetString("extcap-interface") 96 | fifo, _ := pf.GetString("fifo") 97 | extcapControlOut, _ := pf.GetString("extcap-control-out") 98 | extcapControlIn, _ := pf.GetString("extcap-control-in") 99 | eni, _ := pf.GetString("eni") 100 | profile, _ := pf.GetString("profile") 101 | region, _ := pf.GetString("region") 102 | vpc, _ := pf.GetString("vpc") 103 | launchTemplateId, _ := pf.GetString("launch-template-id") 104 | connectivity, _ := pf.GetString("connectivity") 105 | 106 | if cleanup { 107 | return cleanupAll(ctx, profile, region) 108 | } else if extcapInterfaces { 109 | return runExtcapInterfaces(ctx) 110 | } else if extcapConfig { 111 | return runExtcapConfig(ctx, profile, region, vpc, extcapReloadOption) 112 | } else if extcapDLTs { 113 | return runExtcapDLTs(ctx) 114 | } else if capture { 115 | return runCapture(ctx, profile, region, launchTemplateId, connectivity, eni, extcapControlIn, extcapControlOut, fifo) 116 | } else { 117 | return fmt.Errorf("unexpected command") 118 | } 119 | } 120 | 121 | type vpcshark struct { 122 | gui *Wireshark 123 | ec2 *ec2.Client 124 | ssm *ssm.Client 125 | region string 126 | } 127 | 128 | func runExtcapInterfaces(ctx context.Context) error { 129 | fmt.Println(`extcap {version=0.0.1}{help=https://github.com/aidansteele/vpcshark} 130 | interface {value=awsvpc}{display=AWS VPC Traffic Mirroring} 131 | control {number=0}{type=button}{role=logger}{display=Log}{tooltip=Show capture log}`) 132 | return nil 133 | } 134 | 135 | func getTag(tags []types.Tag, name string) string { 136 | for _, tag := range tags { 137 | if aws.ToString(tag.Key) == name { 138 | return aws.ToString(tag.Value) 139 | } 140 | } 141 | 142 | return "" 143 | } 144 | 145 | func runExtcapConfig(ctx context.Context, profile, region, vpc, reloadOption string) error { 146 | opts := []func(*config.LoadOptions) error{ 147 | config.WithSharedConfigProfile(profile), 148 | config.WithRegion(region), 149 | } 150 | 151 | if reloadOption == "vpc" { 152 | cfg, err := config.LoadDefaultConfig(ctx, opts...) 153 | if err != nil { 154 | return fmt.Errorf(": %w", err) 155 | } 156 | 157 | api := ec2.NewFromConfig(cfg) 158 | p := ec2.NewDescribeVpcsPaginator(api, &ec2.DescribeVpcsInput{}) 159 | 160 | lines := []string{} 161 | 162 | for p.HasMorePages() { 163 | page, err := p.NextPage(ctx) 164 | if err != nil { 165 | return fmt.Errorf(": %w", err) 166 | } 167 | 168 | for _, vpc := range page.Vpcs { 169 | vpcId := *vpc.VpcId 170 | display := vpcId 171 | if name := getTag(vpc.Tags, "Name"); name != "" { 172 | display = fmt.Sprintf("%s (%s)", vpcId, name) 173 | } 174 | 175 | lines = append(lines, fmt.Sprintf("value {arg=3}{value=%s}{display=%s}\n", vpcId, display)) 176 | } 177 | } 178 | 179 | sort.Strings(lines) 180 | fmt.Println(strings.Join(lines, "")) 181 | return nil 182 | } 183 | 184 | if reloadOption == "launch-template-id" { 185 | cfg, err := config.LoadDefaultConfig(ctx, opts...) 186 | if err != nil { 187 | return fmt.Errorf(": %w", err) 188 | } 189 | 190 | api := ec2.NewFromConfig(cfg) 191 | p := ec2.NewDescribeLaunchTemplatesPaginator(api, &ec2.DescribeLaunchTemplatesInput{}) 192 | 193 | lines := []string{} 194 | 195 | for p.HasMorePages() { 196 | page, err := p.NextPage(ctx) 197 | if err != nil { 198 | return fmt.Errorf(": %w", err) 199 | } 200 | 201 | for _, template := range page.LaunchTemplates { 202 | id := *template.LaunchTemplateId 203 | name := *template.LaunchTemplateName 204 | lines = append(lines, fmt.Sprintf("value {arg=5}{value=%s}{display=%s (%s)}\n", id, id, name)) 205 | } 206 | } 207 | 208 | sort.Strings(lines) 209 | fmt.Println(strings.Join(lines, "")) 210 | return nil 211 | } 212 | 213 | if reloadOption == "eni" { 214 | cfg, err := config.LoadDefaultConfig(ctx, opts...) 215 | if err != nil { 216 | return fmt.Errorf(": %w", err) 217 | } 218 | 219 | api := ec2.NewFromConfig(cfg) 220 | p := ec2.NewDescribeInstancesPaginator(api, &ec2.DescribeInstancesInput{ 221 | Filters: []types.Filter{ 222 | { 223 | Name: aws.String("vpc-id"), 224 | Values: []string{vpc}, 225 | }, 226 | }, 227 | }) 228 | 229 | lines := []string{} 230 | 231 | for p.HasMorePages() { 232 | page, err := p.NextPage(ctx) 233 | if err != nil { 234 | return fmt.Errorf(": %w", err) 235 | } 236 | 237 | for _, reservation := range page.Reservations { 238 | for _, instance := range reservation.Instances { 239 | for _, networkInterface := range instance.NetworkInterfaces { 240 | eniId := *networkInterface.NetworkInterfaceId 241 | display := fmt.Sprintf("%s: %s", eniId, *instance.InstanceId) 242 | if name := getTag(instance.Tags, "Name"); name != "" { 243 | display = fmt.Sprintf("%s (%s)", display, name) 244 | } 245 | 246 | lines = append(lines, fmt.Sprintf("value {arg=4}{value=%s}{display=%s}\n", eniId, display)) 247 | } 248 | } 249 | } 250 | } 251 | 252 | sort.Strings(lines) 253 | fmt.Println(strings.Join(lines, "")) 254 | return nil 255 | } 256 | 257 | fmt.Printf("arg {number=1}{call=--profile}{group=AWS}{display=AWS Profile}{type=selector}{required=true}\n") 258 | fmt.Printf("arg {number=2}{call=--region}{group=AWS}{display=AWS Region}{type=selector}{required=true}\n") 259 | fmt.Printf("arg {number=3}{call=--vpc}{group=Traffic source}{display=VPC}{type=selector}{required=true}{reload=true}{placeholder=Load VPCs...}\n") 260 | fmt.Printf("arg {number=4}{call=--eni}{group=Traffic source}{display=Capture ENI}{tooltip=Select ENI(s) to monitor}{type=selector}{reload=true}{placeholder=Load ENIs...}{required=true}\n") 261 | fmt.Printf("arg {number=5}{call=--launch-template-id}{group=Mirror target}{display=Launch template}{tooltip=Select launch template for mirror target}{type=selector}{reload=true}{placeholder=Load templates...}{required=true}\n") 262 | fmt.Printf("arg {number=6}{call=--connectivity}{group=Mirror target}{display=Connectivity}{tooltip=Select connectivity to mirror target}{type=selector}{required=true}\n") 263 | fmt.Printf("value {arg=6}{value=public-ssh}{display=Via public IP address over SSH}\n") 264 | fmt.Printf("value {arg=6}{value=ssm-tunnel}{display=Via port-forwarding over AWS Session Manager}\n") 265 | 266 | regionMap, _ := endpoints.RegionsForService(endpoints.DefaultPartitions(), endpoints.AwsPartitionID, "ec2") 267 | regions := []string{} 268 | for r := range regionMap { 269 | regions = append(regions, r) 270 | } 271 | 272 | sort.Strings(regions) 273 | for _, region := range regions { 274 | fmt.Printf("value {arg=2}{value=%s}{display=%s}\n", region, region) 275 | } 276 | 277 | profiles, err := getProfiles(ctx) 278 | if err != nil { 279 | return fmt.Errorf("loading aws profile names: %w", err) 280 | } 281 | 282 | for _, profileName := range profiles { 283 | fmt.Printf("value {arg=1}{value=%s}{display=%s}\n", profileName, profileName) 284 | } 285 | 286 | return nil 287 | } 288 | 289 | func getProfiles(ctx context.Context) ([]string, error) { 290 | cfg, err := ini.Load(config.DefaultSharedConfigFilename()) 291 | if err != nil { 292 | return nil, fmt.Errorf(": %w", err) 293 | } 294 | 295 | profiles := []string{} 296 | 297 | sections := cfg.Sections() 298 | for _, section := range sections { 299 | name := section.Name() 300 | if strings.HasPrefix(name, "profile ") { 301 | profileName := strings.TrimPrefix(name, "profile ") 302 | profiles = append(profiles, profileName) 303 | } 304 | } 305 | 306 | sort.Strings(profiles) 307 | return profiles, nil 308 | } 309 | 310 | func runExtcapDLTs(ctx context.Context) error { 311 | fmt.Println(`dlt {number=1}{name=LINKTYPE_ETHERNET}{display=Ethernet}`) 312 | return nil 313 | } 314 | 315 | func runCapture(ctx context.Context, profile, region, launchTemplateId, connectivity, eni, extcapControlIn, extcapControlOut, fifo string) error { 316 | fmt.Fprintln(os.Stderr, "starting capture") 317 | 318 | controlOut, err := os.OpenFile(extcapControlOut, os.O_WRONLY, os.ModeNamedPipe) 319 | if err != nil { 320 | return fmt.Errorf(": %w", err) 321 | } 322 | 323 | controlIn, err := os.OpenFile(extcapControlIn, os.O_RDONLY, os.ModeNamedPipe) 324 | if err != nil { 325 | return fmt.Errorf(": %w", err) 326 | } 327 | 328 | go handleControlIn(controlIn) 329 | 330 | pcap, err := os.OpenFile(fifo, os.O_WRONLY, os.ModeNamedPipe) 331 | if err != nil { 332 | return fmt.Errorf(": %w", err) 333 | } 334 | 335 | w := &Wireshark{ 336 | controlIn: controlIn, 337 | controlOut: controlOut, 338 | pcap: pcap, 339 | } 340 | w.log = &wiresharkLog{w: w} 341 | 342 | fmt.Fprintf(os.Stderr, "loading aws config\n") 343 | cfg, err := config.LoadDefaultConfig( 344 | ctx, 345 | config.WithSharedConfigProfile(profile), 346 | config.WithRegion(region), 347 | config.WithClientLogMode(aws.LogRequestWithBody|aws.LogResponseWithBody), 348 | //config.WithClientLogMode(aws.LogRequest|aws.LogResponse), 349 | ) 350 | if err != nil { 351 | return fmt.Errorf(": %w", err) 352 | } 353 | 354 | v := &vpcshark{ 355 | gui: w, 356 | region: region, 357 | ec2: ec2.NewFromConfig(cfg), 358 | ssm: ssm.NewFromConfig(cfg), 359 | } 360 | 361 | err = v.ctxmain(ctx, launchTemplateId, connectivity, eni) 362 | if err != nil { 363 | _, _ = fmt.Fprintf(v.gui.Log(), "err: %+v\n", err) 364 | return err 365 | } 366 | 367 | return nil 368 | } 369 | 370 | func (v *vpcshark) startInstance(ctx context.Context, pubkey ssh.PublicKey, launchTemplateId string) (types.Instance, error) { 371 | pubAuthorizedKey := ssh.MarshalAuthorizedKey(pubkey) 372 | 373 | userdata := fmt.Sprintf(`#!/bin/sh 374 | set -eux 375 | 376 | mkdir -p /home/ec2-user/.ssh 377 | echo '%s' >> /home/ec2-user/.ssh/authorized_keys 378 | `, string(pubAuthorizedKey)) 379 | 380 | run, err := v.ec2.RunInstances(ctx, &ec2.RunInstancesInput{ 381 | MinCount: aws.Int32(1), 382 | MaxCount: aws.Int32(1), 383 | UserData: aws.String(base64.StdEncoding.EncodeToString([]byte(userdata))), 384 | InstanceInitiatedShutdownBehavior: types.ShutdownBehaviorTerminate, 385 | LaunchTemplate: &types.LaunchTemplateSpecification{ 386 | LaunchTemplateId: &launchTemplateId, 387 | }, 388 | }) 389 | if err != nil { 390 | return types.Instance{}, fmt.Errorf("launching instance: %w", err) 391 | } 392 | 393 | instance := run.Instances[0] 394 | v.gui.StatusBar(fmt.Sprintf("Started mirror capture instance %s", *instance.InstanceId)) 395 | 396 | return instance, nil 397 | } 398 | 399 | func (v *vpcshark) waitForPublicIp(ctx context.Context, instanceId string) (string, error) { 400 | v.gui.StatusBar(fmt.Sprintf("Waiting for public IP for %s", instanceId)) 401 | 402 | // poll until we have an ec2 instance and public ip address 403 | attempts := 0 404 | for attempts < 10 { 405 | attempts++ 406 | time.Sleep(5 * time.Second) 407 | //fmt.Fprintln(w.Log(), "waiting for instance public ip") 408 | 409 | describe, err := v.ec2.DescribeInstances(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{instanceId}}) 410 | if err != nil { 411 | return "", fmt.Errorf("describing instances: %w", err) 412 | } 413 | 414 | instance := describe.Reservations[0].Instances[0] 415 | if address := aws.ToString(instance.PublicIpAddress); address != "" { 416 | v.gui.StatusBar(fmt.Sprintf("Got public IP %s", address)) 417 | return address, nil 418 | } 419 | } 420 | 421 | return "", fmt.Errorf("couldn't find public ip for instance %s", instanceId) 422 | } 423 | 424 | func (v *vpcshark) waitForManagedInstance(ctx context.Context, instanceId string) error { 425 | // poll until we have a managed instance registered with SSM 426 | attempts := 0 427 | for attempts < 10 { 428 | attempts++ 429 | v.gui.StatusBar(fmt.Sprintf("Waiting for managed instance %s", instanceId)) 430 | time.Sleep(5 * time.Second) 431 | 432 | describe, err := v.ssm.DescribeInstanceInformation(ctx, &ssm.DescribeInstanceInformationInput{ 433 | Filters: []ssmtypes.InstanceInformationStringFilter{ 434 | { 435 | Key: aws.String("InstanceIds"), 436 | Values: []string{instanceId}, 437 | }, 438 | }, 439 | }) 440 | if err != nil { 441 | return fmt.Errorf("listing managed instances: %w", err) 442 | } 443 | 444 | if len(describe.InstanceInformationList) == 0 { 445 | continue 446 | } 447 | 448 | instance := describe.InstanceInformationList[0] 449 | if instance.PingStatus == ssmtypes.PingStatusOnline { 450 | v.gui.StatusBar(fmt.Sprintf("Got managed instance %s", instanceId)) 451 | return nil 452 | } 453 | } 454 | 455 | return fmt.Errorf("couldn't find managed instance %s", instanceId) 456 | } 457 | 458 | func (v *vpcshark) sshClient(ctx context.Context, connectivity string, sshcfg *ssh.ClientConfig, instance types.Instance) (*ssh.Client, error) { 459 | instanceId := *instance.InstanceId 460 | var dial func(ctx context.Context) (net.Conn, error) 461 | 462 | switch connectivity { 463 | case "public-ssh": 464 | publicIp, err := v.waitForPublicIp(ctx, *instance.InstanceId) 465 | if err != nil { 466 | return nil, fmt.Errorf("getting public ip: %w", err) 467 | } 468 | v.gui.StatusBar(fmt.Sprintf("Trying to establish SSH connectivity to %s", publicIp)) 469 | 470 | dial = func(ctx context.Context) (net.Conn, error) { 471 | return (&net.Dialer{}).DialContext(ctx, "tcp", publicIp+":22") 472 | } 473 | case "ssm-tunnel": 474 | err := v.waitForManagedInstance(ctx, instanceId) 475 | if err != nil { 476 | return nil, fmt.Errorf("waiting for managed instance: %w", err) 477 | } 478 | v.gui.StatusBar(fmt.Sprintf("Trying to establish SSH over SSM connectivity to %s", instanceId)) 479 | 480 | dialer := &awsdial.Dialer{Client: v.ssm, Region: v.region} 481 | dial = func(ctx context.Context) (net.Conn, error) { 482 | return dialer.Dial(ctx, instanceId, 22) 483 | } 484 | default: 485 | return nil, fmt.Errorf("unexpected connectivity option: %s", connectivity) 486 | } 487 | 488 | // now we poll until we can connect to the ssh server 489 | attempts := 0 490 | for attempts < 10 { 491 | attempts++ 492 | fmt.Fprintln(v.gui.Log(), time.Now().String()+" waiting for instance ssh connectivity") 493 | 494 | start := time.Now() 495 | 496 | conn, err := dial(ctx) 497 | if err != nil { 498 | time.Sleep(sshcfg.Timeout - time.Now().Sub(start)) 499 | continue 500 | } 501 | 502 | _, _, _, err = ssh.NewClientConn(conn, instanceId, sshcfg) 503 | if err != nil { 504 | conn.Close() 505 | time.Sleep(sshcfg.Timeout - time.Now().Sub(start)) 506 | continue 507 | } 508 | 509 | conn.Close() 510 | break 511 | } 512 | 513 | conn, err := dial(ctx) 514 | if err != nil { 515 | return nil, fmt.Errorf("establishing port forwarding to ec2 instance: %w", err) 516 | } 517 | 518 | cc, chans, reqs, err := ssh.NewClientConn(conn, instanceId, sshcfg) 519 | if err != nil { 520 | return nil, fmt.Errorf("establishing ssh over port-forwarded ssm tunnel: %w", err) 521 | } 522 | 523 | return ssh.NewClient(cc, chans, reqs), nil 524 | } 525 | 526 | func (v *vpcshark) ctxmain(ctx context.Context, launchTemplateId, connectivity, eni string) error { 527 | v.gui.StatusBar("starting") 528 | 529 | describeSourceInstance, err := v.ec2.DescribeInstances(ctx, &ec2.DescribeInstancesInput{ 530 | Filters: []types.Filter{ 531 | { 532 | Name: aws.String("network-interface.network-interface-id"), 533 | Values: []string{eni}, 534 | }, 535 | }, 536 | }) 537 | if err != nil { 538 | return fmt.Errorf("describing mirror source instance: %w", err) 539 | } 540 | 541 | pub, priv, err := ed25519.GenerateKey(rand.Reader) 542 | if err != nil { 543 | return fmt.Errorf("generating ssh keypair: %w", err) 544 | } 545 | 546 | sshpub, err := ssh.NewPublicKey(pub) 547 | if err != nil { 548 | return fmt.Errorf("converting key to ssh: %w", err) 549 | } 550 | 551 | tags := []types.Tag{ 552 | {Key: aws.String("Name"), Value: aws.String("vpcshark")}, 553 | {Key: aws.String("vpcshark"), Value: aws.String("")}, 554 | } 555 | 556 | instance, err := v.startInstance(ctx, sshpub, launchTemplateId) 557 | if err != nil { 558 | return fmt.Errorf(": %w", err) 559 | } 560 | 561 | defer func(ctx context.Context) { 562 | instanceId := *instance.InstanceId 563 | fmt.Fprintf(os.Stderr, "Terminating %s\n", instanceId) 564 | _, err = v.ec2.TerminateInstances(ctx, &ec2.TerminateInstancesInput{InstanceIds: []string{instanceId}}) 565 | fmt.Fprintf(os.Stderr, "Error terminating %s: %+v\n", instanceId, err) 566 | }(ctx) 567 | 568 | captureEni := *instance.NetworkInterfaces[0].NetworkInterfaceId 569 | targetId, cleanup, err := v.createTrafficMirror(ctx, eni, captureEni, tags) 570 | if err != nil { 571 | return err 572 | } 573 | 574 | defer cleanup() 575 | 576 | signer, err := ssh.NewSignerFromSigner(priv) 577 | if err != nil { 578 | return fmt.Errorf("getting ssh signer: %w", err) 579 | } 580 | 581 | sshcfg := &ssh.ClientConfig{ 582 | User: "ec2-user", 583 | Timeout: 5 * time.Second, 584 | Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, 585 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { 586 | // allow any host key 587 | return nil 588 | }, 589 | } 590 | 591 | client, err := v.sshClient(ctx, connectivity, sshcfg, instance) 592 | if err != nil { 593 | return fmt.Errorf(": %w", err) 594 | } 595 | defer client.Close() 596 | 597 | err = v.installRemoteTool(ctx, client) 598 | if err != nil { 599 | return fmt.Errorf("installing socat: %w", err) 600 | } 601 | 602 | v.gui.StatusBar("Good to go") 603 | 604 | ctx, cancel := context.WithCancel(ctx) 605 | g, ctx := errgroup.WithContext(ctx) 606 | pktch := make(chan []byte) 607 | 608 | g.Go(func() error { 609 | defer cancel() 610 | err := v.writePcapToFifo(ctx, eni, *describeSourceInstance.Reservations[0].Instances[0].InstanceId, pktch, v.gui.Pcap()) 611 | if errors.Is(err, syscall.EPIPE) { 612 | fmt.Fprintln(os.Stderr, "Wireshark closed pcap fifo") 613 | return nil 614 | } 615 | 616 | return err 617 | }) 618 | 619 | g.Go(func() error { 620 | return v.runRemoteTool(ctx, client, targetId, pktch) 621 | }) 622 | 623 | err = g.Wait() 624 | return err 625 | } 626 | 627 | func (v *vpcshark) createTrafficMirror(ctx context.Context, mirroredEni, captureEni string, tags []types.Tag) (targetId string, cleanup func(), err error) { 628 | target, err := v.ec2.CreateTrafficMirrorTarget(ctx, &ec2.CreateTrafficMirrorTargetInput{ 629 | NetworkInterfaceId: &captureEni, 630 | Description: aws.String(trafficMirrorDescription), 631 | TagSpecifications: []types.TagSpecification{ 632 | {ResourceType: types.ResourceTypeTrafficMirrorTarget, Tags: tags}, 633 | }, 634 | }) 635 | if err != nil { 636 | return "", cleanup, fmt.Errorf("creating traffic mirror target: %w", err) 637 | } 638 | 639 | targetId = *target.TrafficMirrorTarget.TrafficMirrorTargetId 640 | v.gui.StatusBar(fmt.Sprintf("Created traffic mirror target %s", targetId)) 641 | 642 | filter, err := v.ec2.CreateTrafficMirrorFilter(ctx, &ec2.CreateTrafficMirrorFilterInput{ 643 | Description: aws.String(trafficMirrorDescription), 644 | TagSpecifications: []types.TagSpecification{ 645 | {ResourceType: types.ResourceTypeTrafficMirrorFilter, Tags: tags}, 646 | }, 647 | }) 648 | if err != nil { 649 | return "", cleanup, fmt.Errorf("creating traffic mirror filter: %w", err) 650 | } 651 | 652 | filterId := filter.TrafficMirrorFilter.TrafficMirrorFilterId 653 | v.gui.StatusBar(fmt.Sprintf("Created traffic mirror filter %s", *filterId)) 654 | 655 | _, err = v.ec2.ModifyTrafficMirrorFilterNetworkServices(ctx, &ec2.ModifyTrafficMirrorFilterNetworkServicesInput{ 656 | TrafficMirrorFilterId: filterId, 657 | AddNetworkServices: []types.TrafficMirrorNetworkService{types.TrafficMirrorNetworkServiceAmazonDns}, 658 | }) 659 | if err != nil { 660 | return "", cleanup, fmt.Errorf("enabling dns resolution in mirror filter: %w", err) 661 | } 662 | 663 | _, err = v.ec2.CreateTrafficMirrorFilterRule(ctx, &ec2.CreateTrafficMirrorFilterRuleInput{ 664 | TrafficMirrorFilterId: filterId, 665 | DestinationCidrBlock: aws.String("0.0.0.0/0"), 666 | SourceCidrBlock: aws.String("0.0.0.0/0"), 667 | TrafficDirection: types.TrafficDirectionIngress, 668 | RuleAction: types.TrafficMirrorRuleActionAccept, 669 | RuleNumber: aws.Int32(100), 670 | }) 671 | if err != nil { 672 | return "", cleanup, fmt.Errorf("creating filter ingress rule: %w", err) 673 | } 674 | 675 | _, err = v.ec2.CreateTrafficMirrorFilterRule(ctx, &ec2.CreateTrafficMirrorFilterRuleInput{ 676 | TrafficMirrorFilterId: filterId, 677 | DestinationCidrBlock: aws.String("0.0.0.0/0"), 678 | SourceCidrBlock: aws.String("0.0.0.0/0"), 679 | TrafficDirection: types.TrafficDirectionEgress, 680 | RuleAction: types.TrafficMirrorRuleActionAccept, 681 | RuleNumber: aws.Int32(100), 682 | }) 683 | if err != nil { 684 | return "", cleanup, fmt.Errorf("creating filter egress rule: %w", err) 685 | } 686 | 687 | session, err := v.ec2.CreateTrafficMirrorSession(ctx, &ec2.CreateTrafficMirrorSessionInput{ 688 | NetworkInterfaceId: &mirroredEni, 689 | SessionNumber: aws.Int32(int32(1_000 + mrand.Intn(30_000))), 690 | TrafficMirrorFilterId: filterId, 691 | TrafficMirrorTargetId: &targetId, 692 | VirtualNetworkId: aws.Int32(trafficMirrorVNI), 693 | Description: aws.String(trafficMirrorDescription), 694 | TagSpecifications: []types.TagSpecification{ 695 | {ResourceType: types.ResourceTypeTrafficMirrorSession, Tags: tags}, 696 | }, 697 | }) 698 | if err != nil { 699 | return "", cleanup, fmt.Errorf("creating traffic mirror session: %w", err) 700 | } 701 | 702 | sessionId := session.TrafficMirrorSession.TrafficMirrorSessionId 703 | v.gui.StatusBar(fmt.Sprintf("Created traffic mirror session %s", *sessionId)) 704 | 705 | cleanup = func() { 706 | fmt.Fprintf(os.Stderr, "Deleting %s\n", *sessionId) 707 | v.ec2.DeleteTrafficMirrorSession(ctx, &ec2.DeleteTrafficMirrorSessionInput{TrafficMirrorSessionId: sessionId}) 708 | 709 | fmt.Fprintf(os.Stderr, "Deleting %s\n", *filterId) 710 | v.ec2.DeleteTrafficMirrorFilter(ctx, &ec2.DeleteTrafficMirrorFilterInput{TrafficMirrorFilterId: filterId}) 711 | 712 | fmt.Fprintf(os.Stderr, "Deleting %s\n", targetId) 713 | v.ec2.DeleteTrafficMirrorTarget(ctx, &ec2.DeleteTrafficMirrorTargetInput{TrafficMirrorTargetId: &targetId}) 714 | } 715 | 716 | return targetId, cleanup, nil 717 | } 718 | 719 | func (v *vpcshark) runRemoteTool(ctx context.Context, client *ssh.Client, targetId string, packets chan []byte) error { 720 | sess, err := client.NewSession() 721 | if err != nil { 722 | return fmt.Errorf("opening ssh session for remote tool: %w", err) 723 | } 724 | defer sess.Close() 725 | 726 | go func() { 727 | <-ctx.Done() 728 | sess.Close() 729 | }() 730 | 731 | sess.Stdout = os.Stderr 732 | sess.Stderr = os.Stderr 733 | 734 | err = sess.Run(fmt.Sprintf("%s %s %s", remotePath, v.region, targetId)) 735 | if err != nil { 736 | return fmt.Errorf("running remote cmd: %w", err) 737 | } 738 | 739 | time.Sleep(time.Second) 740 | 741 | conn, err := client.Dial("tcp", "127.0.0.1:4790") 742 | if err != nil { 743 | return fmt.Errorf("dialing 4790: %w", err) 744 | } 745 | defer conn.Close() 746 | 747 | go func() { 748 | <-ctx.Done() 749 | conn.Close() 750 | }() 751 | 752 | lenbuf := make([]byte, 2) 753 | for { 754 | _, err := conn.Read(lenbuf) 755 | if err != nil { 756 | return fmt.Errorf("reading conn: %w", err) 757 | } 758 | 759 | pktlen := binary.BigEndian.Uint16(lenbuf) 760 | pkt := make([]byte, pktlen) 761 | _, err = io.ReadFull(conn, pkt) 762 | if err != nil { 763 | return fmt.Errorf(": %w", err) 764 | } 765 | 766 | select { 767 | case <-ctx.Done(): 768 | return nil 769 | case packets <- pkt: 770 | // no-op 771 | } 772 | } 773 | } 774 | 775 | func (v *vpcshark) writePcapToFifo(ctx context.Context, eni, instanceId string, packets chan []byte, fifo io.WriteCloser) error { 776 | w, err := pcapgo.NewNgWriterInterface(fifo, pcapgo.NgInterface{ 777 | Name: eni, 778 | Description: instanceId, 779 | LinkType: layers.LinkTypeEthernet, 780 | Statistics: pcapgo.NgInterfaceStatistics{}, 781 | }, pcapgo.NgWriterOptions{}) 782 | if err != nil { 783 | return fmt.Errorf(": %w", err) 784 | } 785 | 786 | for { 787 | select { 788 | case <-ctx.Done(): 789 | return nil 790 | case pktbuf := <-packets: 791 | pkt := gopacket.NewPacket(pktbuf, layers.LayerTypeVXLAN, gopacket.Default) 792 | vxlan := pkt.Layers()[0].(*layers.VXLAN) 793 | payload := vxlan.LayerPayload() 794 | 795 | err = w.WritePacket(gopacket.CaptureInfo{ 796 | Timestamp: time.Now(), 797 | CaptureLength: len(payload), 798 | Length: len(payload), 799 | //InterfaceIndex: 0, 800 | //AncillaryData: nil, 801 | }, payload) 802 | if err != nil { 803 | return fmt.Errorf("write : %w", err) 804 | } 805 | 806 | err = w.Flush() 807 | if err != nil { 808 | return fmt.Errorf("flush : %w", err) 809 | } 810 | 811 | if err != nil { 812 | return fmt.Errorf("sync : %w", err) 813 | } 814 | } 815 | } 816 | } 817 | 818 | //go:embed remote/remote 819 | var remoteBinary []byte 820 | 821 | const remotePath = "/tmp/vpcshark-remote" 822 | 823 | func (v *vpcshark) installRemoteTool(ctx context.Context, client *ssh.Client) error { 824 | v.gui.StatusBar("Copying remote helper to instance") 825 | 826 | c, err := scp.NewClientBySSH(client) 827 | if err != nil { 828 | return fmt.Errorf("creating scp client: %w", err) 829 | } 830 | 831 | err = c.Copy(ctx, bytes.NewReader(remoteBinary), remotePath, "0755", int64(len(remoteBinary))) 832 | if err != nil { 833 | return fmt.Errorf("copying remote binary over scp: %w", err) 834 | } 835 | 836 | return nil 837 | } 838 | -------------------------------------------------------------------------------- /wireshark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | ) 12 | 13 | type Wireshark struct { 14 | controlIn *os.File 15 | controlOut *os.File 16 | pcap io.WriteCloser 17 | log *wiresharkLog 18 | } 19 | 20 | type wiresharkLog struct { 21 | w *Wireshark 22 | } 23 | 24 | func (w *wiresharkLog) Write(p []byte) (n int, err error) { 25 | number := 0 // TODO: not always zero 26 | 27 | err = w.w.ControlWrite(byte(number), 2, p) 28 | if err == nil { 29 | n = len(p) 30 | } 31 | 32 | return 33 | } 34 | 35 | func (w *Wireshark) Pcap() io.WriteCloser { 36 | return w.pcap 37 | } 38 | 39 | func (w *Wireshark) Log() io.Writer { 40 | return w.log 41 | } 42 | 43 | func (w *Wireshark) ControlWrite(control, command byte, payload []byte) error { 44 | if len(payload) > 65535 { 45 | return fmt.Errorf("payload must be 0-65535 bytes") 46 | } 47 | 48 | buf := &bytes.Buffer{} 49 | 50 | // sync pipe indication (1 byte) 51 | buf.WriteByte('T') 52 | 53 | // message length (3 bytes) 54 | msglen := 2 + len(payload) 55 | lenbuf := make([]byte, 4) 56 | binary.BigEndian.PutUint32(lenbuf, uint32(msglen)) 57 | buf.WriteByte(lenbuf[1]) 58 | buf.WriteByte(lenbuf[2]) 59 | buf.WriteByte(lenbuf[3]) 60 | 61 | // control number (1 byte) 62 | buf.WriteByte(control) 63 | 64 | // command (1 byte) 65 | buf.WriteByte(command) 66 | 67 | // payload (0 - 64K bytes) 68 | buf.Write(payload) 69 | 70 | _, err := io.Copy(w.controlOut, buf) 71 | return err 72 | } 73 | 74 | func (w *Wireshark) StatusBar(msg string) { 75 | err := w.ControlWrite(0, 6, []byte(msg)) 76 | if err != nil { 77 | panic(fmt.Sprintf("%+v", err)) 78 | } 79 | } 80 | 81 | func handleControlIn(ctlInPipe *os.File) { 82 | hdr := make([]byte, 6) 83 | payload := make([]byte, 65535) 84 | for { 85 | _, err := io.ReadAtLeast(ctlInPipe, hdr, 6) 86 | if errors.Is(err, io.EOF) { 87 | return 88 | } 89 | if err != nil { 90 | panic(fmt.Sprintf("%+v", err)) 91 | } 92 | 93 | if hdr[0] != 'T' { 94 | panic("didn't get expected sync pipe indication") 95 | } 96 | 97 | hdr[0] = 0 98 | pktlen := binary.BigEndian.Uint32(hdr[0:4]) - 2 99 | _, err = io.ReadAtLeast(ctlInPipe, payload, int(pktlen)) 100 | if err != nil { 101 | panic(fmt.Sprintf("%+v", err)) 102 | } 103 | 104 | controlNum := hdr[4] 105 | command := hdr[5] 106 | 107 | fmt.Fprintf(os.Stderr, "ctrl num=%d command=%d payload=%s\n", controlNum, command, hex.EncodeToString(payload[:pktlen])) 108 | } 109 | } 110 | --------------------------------------------------------------------------------