Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
aidansteele committed Jun 15, 2023
0 parents commit 347ca97
Show file tree
Hide file tree
Showing 9 changed files with 558 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: release

on:
push:
tags:
- '*'

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- run: git fetch --force --tags

- uses: actions/setup-go@v4
with:
go-version: stable

- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

dist/
54 changes: 54 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- arm64
- amd64
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
brews:
- name: rdsconn
tap:
owner: aidansteele
name: homebrew-taps
commit_author:
name: Aidan Steele
email: [email protected]
homepage: https://github.com/aidansteele/rdsconn
description: rdsconn makes connecting to an AWS RDS instance inside a VPC from your laptop easier
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# rdsconn

On June 14th, 2023 AWS [launched][aws-blog] new connectivity options for
_EC2 Instance Connect_. This functionality also works for non-EC2 resources in
VPCs. You _could_ run the official AWS CLI (>= v2.12.0) using the following command,
but `rdsconn` aims to make the RDS experience easier.

```
aws ec2-instance-connect open-tunnel \
--private-ip-address 10.1.2.150 \
--instance-connect-endpoint-id eice-06d8b7ad48example \
--remote-port 5432 \
--local-port 5432
```

## Installation

On macOS, `brew install aidansteele/taps/rdsconn`. On other platforms: see
published binaries in the releases tab of the GitHub repo.

## Usage

1. Create an EC2 Instance Connect endpoint in your VPC. Ensure that your RDS DB
instance's security group allows the EIC endpoint to connect to it.
2. Have valid AWS credentials configured. E.g. either as environment variables,
default credentials in your config file, or a profile with `AWS_PROFILE=name`
env var set.
3. Run `rdsconn proxy`. The CLI will prompt you to select an RDS DB instance from
the list of DBs in your account. Hit enter to confirm selection.
4. The message `Proxy running. Now waiting to serve connections to localhost:5432...`
will appear. You can now run `psql ... -h 127.0.0.1` (or `mysql ...`)

## Future plans

* Flesh out this README more
* Detect incorrect configurations and provide helpful error messages to user.
E.g. missing endpoints, security groups, routes, etc.
* Add a `client` subcommand that uses RDS IAM authentication to launch and
authenticate a child process `psql` CLI (using PGPASSWORD etc env vars)

[aws-blog]: https://aws.amazon.com/blogs/compute/secure-connectivity-from-public-to-private-introducing-ec2-instance-connect-endpoint-june-13-2023/
115 changes: 115 additions & 0 deletions ec2ic/dialer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package ec2ic

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/gorilla/websocket"
"io"
"net"
"net/http"
"net/url"
"time"
)

type Dialer struct {
cfg aws.Config
endpointId string
endpointDns string
duration time.Duration
}

func NewDialer(ctx context.Context, cfg aws.Config, endpointId string, duration time.Duration) (*Dialer, error) {
api := ec2.NewFromConfig(cfg)

if duration == 0 {
duration = time.Hour
}

describe, err := api.DescribeInstanceConnectEndpoints(ctx, &ec2.DescribeInstanceConnectEndpointsInput{InstanceConnectEndpointIds: []string{endpointId}})
if err != nil {
return nil, fmt.Errorf("describing ec2 instance connect endpoint: %w", err)
}

return &Dialer{
cfg: cfg,
endpointId: endpointId,
endpointDns: *describe.InstanceConnectEndpoints[0].DnsName,
duration: duration,
}, nil
}

func (icd *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
if network != "tcp" && network != "tcp4" {
return nil, fmt.Errorf("only tcp supported")
}

host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, fmt.Errorf("splitting host and port: %w", err)
}

resolved, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, fmt.Errorf("resolving private ip: %w", err)
}

q := url.Values{}
q.Set("instanceConnectEndpointId", icd.endpointId)
q.Set("maxTunnelDuration", fmt.Sprintf("%d", int(icd.duration.Seconds())))
q.Set("remotePort", port)
q.Set("privateIpAddress", resolved.String())

r, _ := http.NewRequest("GET", fmt.Sprintf("wss://%s/openTunnel?%s", icd.endpointDns, q.Encode()), nil)

sum := sha256.Sum256([]byte{})
sumStr := hex.EncodeToString(sum[:])

creds, err := icd.cfg.Credentials.Retrieve(ctx)
if err != nil {
panic(fmt.Sprintf("%+v", err))
}

s := v4.NewSigner()
signed, _, err := s.PresignHTTP(ctx, creds, r, sumStr, "ec2-instance-connect", icd.cfg.Region, time.Now())
if err != nil {
panic(fmt.Sprintf("%+v", err))
}

conn, _, err := websocket.DefaultDialer.DialContext(ctx, signed, http.Header{})
if err != nil {
return nil, fmt.Errorf("dialing websocket: %w", err)
}

return &icdConn{
Conn: conn,
r: websocket.JoinMessages(conn, ""),
}, nil
}

type icdConn struct {
*websocket.Conn
r io.Reader
}

var _ net.Conn = (*icdConn)(nil)

func (i *icdConn) Read(b []byte) (n int, err error) {
return i.r.Read(b)
}

func (i *icdConn) Write(b []byte) (n int, err error) {
err = i.Conn.WriteMessage(websocket.BinaryMessage, b)
n = len(b)
return
}

func (i *icdConn) SetDeadline(t time.Time) error {
i.SetReadDeadline(t)
i.SetWriteDeadline(t)
return nil
}
29 changes: 29 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module github.com/aidansteele/rdsconn

go 1.20

require (
github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.26 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.25 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/service/ec2 v1.100.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
github.com/aws/aws-sdk-go-v2/service/rds v1.45.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.1 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
)
58 changes: 58 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/config v1.18.26 h1:ivCHcSmKd1+9rBlqVsxZHB35eCW88KWbMdG2VL3BuBw=
github.com/aws/aws-sdk-go-v2/config v1.18.26/go.mod h1:NVmd//z/PNl7U+ZU2EnuffxOA060JWzgbH3BnqQrUoY=
github.com/aws/aws-sdk-go-v2/credentials v1.13.25 h1:5wROoMcUC7nAE66e0b3IIht6Tos76M4HC+GQw8MeqxU=
github.com/aws/aws-sdk-go-v2/credentials v1.13.25/go.mod h1:W9I2660WXSwZQ23mM1Ks72+UGeyirIxuU7/KzN7daeA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.100.0 h1:Wo/HHkC8PcVPXAB9WNwMmGHiA+b6VsJiT/DW2nB2D1w=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.100.0/go.mod h1:tIctCeX9IbzsUTKHt53SVEcgyfxV2ElxJeEB+QUbc4M=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
github.com/aws/aws-sdk-go-v2/service/rds v1.45.1 h1:Pxwe38BtUudOFqCNBFrzw0Gxufs0YioaTFnpubJTg7Y=
github.com/aws/aws-sdk-go-v2/service/rds v1.45.1/go.mod h1:goBDR4OPrsnKpYyU0GHGcEnlTmL8O+eKGsWeyOAFJ5M=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.11 h1:cNrMc266RsZJ8V1u1OQQONKcf9HmfxQFqgcpY7ZJBhY=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.11/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.11 h1:h2VhtCE5PBiJefmlVCjJRSzBfFcQeAE10SXIGkXw1jQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.11/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.1 h1:ehPTnLR/es8TL1fpBfq8qw9cAwOpQr47fLmZD9yhHjk=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.1/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
57 changes: 57 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/rds"
"github.com/spf13/cobra"
)

func main() {
rootCmd := &cobra.Command{
RunE: runList,
}

proxyCmd := &cobra.Command{
Use: "proxy",
Aliases: []string{"p"},
RunE: runProxy,
}

proxyCmd.PersistentFlags().String("endpoint-id", "", "")
proxyCmd.PersistentFlags().Int("local-port", 0, "")

rootCmd.AddCommand(proxyCmd)

ctx := context.Background()
err := rootCmd.ExecuteContext(ctx)
if err != nil {
panic(fmt.Sprintf("%+v", err))
}
}

func runList(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return fmt.Errorf(": %w", err)
}

rdsapi := rds.NewFromConfig(cfg)

p := rds.NewDescribeDBInstancesPaginator(rdsapi, &rds.DescribeDBInstancesInput{})
for p.HasMorePages() {
page, err := p.NextPage(ctx)
if err != nil {
return fmt.Errorf(": %w", err)
}

for _, instance := range page.DBInstances {
fmt.Fprintln(cmd.OutOrStdout(), *instance.DBInstanceIdentifier)
}
}

return nil
}
Loading

0 comments on commit 347ca97

Please sign in to comment.