├── .gitmodules ├── ca-key.yml ├── docs ├── README.md ├── authorizer.png ├── aws-diagram.png ├── geneve-packet-http2.png ├── sts-rickroll.png ├── upside-down.png ├── vendors-aws-blog.png └── wireshark-demo.png ├── examples ├── account_id_emf │ ├── account_id_emf.go │ └── parse.go ├── cloudfront_functions │ ├── cloudfront_functions.go │ ├── cloudfront_functions_test.go │ ├── rick.go │ └── rick.js ├── flowdogshark │ └── flowdogshark.go ├── geneve_headers │ └── geneve_headers.go ├── lambda_acceptor │ ├── cfn.yml │ ├── enricher.go │ ├── lambda │ │ └── lambda.go │ └── lambda_acceptor.go ├── sts_rickroll │ └── sts_rickroll.go ├── upsidedown │ └── upsidedown.go └── webfirehose │ ├── webfirehose.go │ └── webfirehose.yml ├── go.mod ├── go.sum ├── gwlb ├── active_flow.go ├── context.go ├── default_handler.go ├── dial.go ├── fastpath.go ├── geneve_options.go ├── interceptors.go ├── mirror │ └── mirrored_packet.go ├── serve.go ├── shark │ ├── filter.go │ ├── shark.go │ ├── shark.pb.go │ ├── shark.proto │ └── shark_grpc.pb.go ├── udpconn_map.go └── websocket.go ├── kmssigner ├── generate │ └── generate.go └── kmssigner.go ├── main.go └── mytls ├── intermediate.go └── tls_config.go /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "forks/gopacket"] 2 | path = forks/gopacket 3 | url = git@github.com:aidansteele/gopacket.git 4 | -------------------------------------------------------------------------------- /ca-key.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | 3 | Resources: 4 | Alias: 5 | Type: AWS::KMS::Alias 6 | Properties: 7 | AliasName: alias/flowdog-ca 8 | TargetKeyId: !Ref Key 9 | 10 | Key: 11 | Type: AWS::KMS::Key 12 | Properties: 13 | Description: Flowdog Root CA 14 | KeySpec: ECC_NIST_P256 15 | KeyUsage: SIGN_VERIFY 16 | KeyPolicy: 17 | Version: "2012-10-17" 18 | Id: key-policy 19 | Statement: 20 | - Sid: AllowIAM 21 | Effect: Allow 22 | Action: kms:* 23 | Resource: "*" 24 | Principal: 25 | AWS: !Sub arn:aws:iam::${AWS::AccountId}:root 26 | 27 | Outputs: 28 | Alias: 29 | Value: !Ref Alias 30 | Key: 31 | Value: !GetAtt Key.Arn 32 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | [Twitter thread](https://twitter.com/__steele/status/1481227583274758146) 2 | 3 | # flowdog 4 | 5 | This is an application/framework for inspection and manipulation of network 6 | traffic in AWS VPCs. Packets routed to or from the Internet, between VPCs, 7 | between subnets can all be programmatically inspected or modified in great 8 | detail. 9 | 10 | This is achieved via AWS Gateway Load Balancers. GWLBs are a cloud-native 11 | alternative to NAT instances. They can auto-scale, they can be highly available 12 | across availability zones and they can even be provided as managed services 13 | from entirely separate AWS accounts. 14 | 15 | But they're hard to use*. This project tries to make them easier. See further 16 | down for an explanation of the difficulty. 17 | 18 | ## Example use cases 19 | 20 | These are really just intended to demonstrate that anything is possible in the 21 | world of software-defined networking. Please ping me on [Twitter][twit] with any 22 | cool ideas you have. Or any enhancements to the following ideas. 23 | 24 | * [`lambda_acceptor/lambda_acceptor.go`](/examples/lambda_acceptor/lambda_acceptor.go) 25 | takes the idea of [AWS API Gateway Lambda authorizers][apigw-auth] and applies 26 | it to VPC flows. At the start of every new connection, a Lambda function is 27 | invoked and returns a decision about whether to allow or drop the connection. 28 | It's like security groups 2.0. Input/output looks like this: 29 | 30 | ![authorizer-io](authorizer.png) 31 | 32 | * [`cloudfront_functions/rick.js`](/examples/cloudfront_functions/rick.js) is 33 | an example of how the [CloudFront Functions][cff-model] event model can be 34 | applied to rewriting HTTP(S) requests inside a VPC. In this particular example, 35 | we're ensuring that any [AWS Workspaces][workspaces] users visiting YouTube 36 | can only watch one particular video. 37 | 38 | * [`flowdogshark/flowdogshark.go`](/examples/flowdogshark/flowdogshark.go) is an 39 | [`extcap`][extcap] plugin for Wireshark that allows you to live-attach 40 | Wireshark to flowdog and capture traffic flowing through your VPC. Given that 41 | flowdog does TLS interception (see later section in README), it can even use 42 | Wireshark's support for decoding TLS. Here's an example of intercepting the 43 | Amazon SSM agent: 44 | 45 | ![wireshark demo](wireshark-demo.png) 46 | 47 | * [`account_id_emf/account_id_emf.go`](/examples/account_id_emf/account_id_emf.go) 48 | is an example of scanning all AWS API calls made within the VPC for SigV4 auth 49 | headers, [extracting the AWS account ID][extract-acct-id] and emitting it to 50 | CloudWatch via specially-formatted logs that are turned into metrics. This could 51 | be used to alert on newly-seen account IDs: a potential indicator of a compromised 52 | instance. 53 | 54 | * [`upsidedown/upsidedown.go`](/examples/upsidedown/upsidedown.go) is an 55 | implementation of the classic [Upside-Down-Ternet][upsidedown]. It blurs and 56 | rotates every image 180º when browsing the net. 57 | 58 | ![upside down](upside-down.png) 59 | 60 | * [`sts_rickroll/sts_rickroll.go`](/examples/sts_rickroll/sts_rickroll.go) is 61 | another silly example. Here we are modifying the response of the AWS API call 62 | for `aws sts get-caller-identity` to return something unexpected. You could 63 | equally use the same logic to return your favourite video on every seventh 64 | object downloaded through an S3 VPC gateway. 65 | 66 | ![sts-rickroll](sts-rickroll.png) 67 | 68 | * [`gwlb/websocket.go`](/gwlb/websocket.go) is not an example, but I got lazy. 69 | [Nick Frichette][nickf] had the great suggestion of intercepting the [SSM agent][agent] 70 | for shenanigans. This code will detect websockets and parse messages, but right 71 | now only passes them back and forth. Soon™. 72 | 73 | * TODO: You could save [HAR archives][har] of all web traffic to buckets 74 | in S3 for later perusal. 75 | 76 | ## What about TLS? 77 | 78 | As great as GWLBs are, they're not magic. We haven't broken TLS. For this app, 79 | we create a custom root certificate authority and add it to the trust store on 80 | our EC2 instances. Rather than deal in sensitive private key material, we use 81 | AWS [KMS' support for asymmetric keys][kms] for our private key. 82 | [`generate.go`](/kmssigner/generate/generate.go) creates a certificate using that 83 | key. That certificate is then stored and trusted on the OS (e.g. in Amazon Linux 2 84 | you would run `cat $CERT >> /usr/share/pki/ca-trust-source/anchors/lol.pem && update-ca-trust`) 85 | 86 | Rather than invoking KMS on every TLS connection, on launch this app creates an 87 | ephemeral key pair and certificate in memory, asks KMS to sign it and then uses 88 | that as an intermediate certificate authority. This means we can have fast TLS 89 | de/re-encryption with no stored secrets. 90 | 91 | When Wireshark is attached, flowdog can stream TLS key logs in [NSS Key Log Format][klf]. 92 | This allows the Wireshark user to view all decrypted TLS traffic without giving 93 | away either the KMS private key (impossible) or intermediate CA private key (very 94 | unwise). 95 | 96 | ## Why so hard? 97 | 98 | (*) GWLBs aren't hard themselves, look at this diagram (from Amazon's [blog post][amz-blog]): 99 | 100 | ![amazon diagram](aws-diagram.png) 101 | 102 | It's inspecting and modifying network traffic in general that is extremely 103 | difficult. Especially non-trivial modifications. Take the following diagram as 104 | an example. This is just one packet in a flow of packets between an EC2 105 | instance and the Internet when `curl https://google.com` is run. 106 | 107 | ![packet diagram](geneve-packet-http2.png) 108 | 109 | You can think of this packet as having many layers. Each layer "wraps" the 110 | layer below it. The bottom six layers were sent by the EC2 instance. The top 111 | three layers are GWLB-specific. They identify which VPC endpoint (e.g. customer) 112 | the packet came from and which "flow" of packets this particular packet belongs to. 113 | 114 | Say we want to change all web requests to google.com to have the `User-Agent` 115 | request header instead be lower-case, e.g. `user-agent`. This would require us to 116 | parse the formats for: 117 | 118 | * The inner IPv4 layer, to identify is this a TCP packet 119 | * The TCP layer, to identify if the destination port is 80 or 443 120 | * The TLS layer, to (magically, for now) decrypt the payload 121 | * The HTTP/2 layer, to inspect the multiplexed streams within 122 | * The frames in each HTTP/2 stream, to identify if they are a `HEADERS` frame. 123 | * The headers in the HTTP/2 frame, to see if the `User-Agent` header is present. 124 | 125 | Finally we would have to edit the packet in memory at the right offset to change 126 | `U` to `u` and `A` to `a`, correct the checksums at every layer of the packet 127 | and re-encrypt the TLS payload. That's a lot of work. 128 | 129 | And that's a trivial change: the packet length hasn't changed. Imagine if 130 | wanted to insert a few additional headers in that request. Maybe that would 131 | push the packet length over the typical 1500 byte limit for packets on the 132 | Internet. That increases the amount of work needed by orders of magnitude: now 133 | we need to reimplement the TCP state machine, because we'll now need two packets. 134 | And those packets each need sequence numbers. But the original EC2 instance will 135 | get a response from Google for sequence numbers it didn't expect, so the 136 | connection will fail. So what we need to do is instead _terminate_ the TCP 137 | connection at the GWLB appliance and open a _new_ connection to Google from the 138 | GWLB appliance. The app will need to juggle these two TCP connections and pass 139 | the underlying data to and from Google and the EC2 instance, all while keeping 140 | the two connection's different states in sync. 141 | 142 | That's so much work that it's no wonder that even after more than a year, only 143 | massive well-funded vendors have implemented this capability. And even then, it 144 | looks like they're limited to either read-only inspection or dropping suspicious 145 | packets. 146 | 147 | ![vendors](vendors-aws-blog.png) 148 | 149 | So I built this thing. It uses a handful of packages to make traffic inspection 150 | and modification accessible to even developers like you or me. Those packages 151 | are: 152 | 153 | * [`inet.af/netstack`](https://pkg.go.dev/inet.af/netstack): a reimplementation 154 | of the entire Linux TCP/IP stack in Go, extracted from the gVisor project. 155 | 156 | * [`github.com/google/gopacket`](https://pkg.go.dev/github.com/google/gopacket) 157 | to extract and parse the Geneve, IP, TCP, UDP, etc layers from the raw packets 158 | delivered by the GWLB. 159 | 160 | * [`httputil`](https://pkg.go.dev/net/http/httputil) in the Go stdlib, to 161 | reverse-proxy HTTP and HTTPS traffic and parse flows into individual request 162 | and response objects. 163 | 164 | * [`github.com/aws/aws-sdk-go`](https://pkg.go.dev/github.com/aws/aws-sdk-go) to 165 | use AWS KMS asymmetric keys for the root certificate authority that can be 166 | installed on EC2 instances for transparent TLS decryption - without having to 167 | manage a highly-sensitive private key. 168 | 169 | * [`rogchap.com/v8go`](https://pkg.go.dev/rogchap.com/v8go) to embed the V8 170 | JavaScript engine into Go, so that we can write scripts to modify traffic 171 | in JS, which is more familiar than Go to many developers. 172 | 173 | [extcap]: https://www.wireshark.org/docs/man-pages/extcap.html 174 | [klf]: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format 175 | [twit]: https://twitter.com/__steele 176 | [kms]: https://docs.aws.amazon.com/kms/latest/developerguide/symmetric-asymmetric.html 177 | [cff-model]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/writing-function-code.html 178 | [workspaces]: https://aws.amazon.com/workspaces/ 179 | [extract-acct-id]: https://awsteele.com/blog/2020/09/26/aws-access-key-format.html 180 | [apigw-auth]: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html 181 | [nickf]: https://github.com/Frichetten 182 | [agent]: https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html 183 | [har]: https://en.wikipedia.org/wiki/HAR_(file_format) 184 | [amz-blog]: https://aws.amazon.com/blogs/networking-and-content-delivery/integrate-your-custom-logic-or-appliance-with-aws-gateway-load-balancer/ 185 | [upsidedown]: https://www.ex-parrot.com/pete/upside-down-ternet.html 186 | -------------------------------------------------------------------------------- /docs/authorizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/flowdog/b58f75107233d557453b9bffaa8198f76a9d9ed2/docs/authorizer.png -------------------------------------------------------------------------------- /docs/aws-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/flowdog/b58f75107233d557453b9bffaa8198f76a9d9ed2/docs/aws-diagram.png -------------------------------------------------------------------------------- /docs/geneve-packet-http2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/flowdog/b58f75107233d557453b9bffaa8198f76a9d9ed2/docs/geneve-packet-http2.png -------------------------------------------------------------------------------- /docs/sts-rickroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/flowdog/b58f75107233d557453b9bffaa8198f76a9d9ed2/docs/sts-rickroll.png -------------------------------------------------------------------------------- /docs/upside-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/flowdog/b58f75107233d557453b9bffaa8198f76a9d9ed2/docs/upside-down.png -------------------------------------------------------------------------------- /docs/vendors-aws-blog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/flowdog/b58f75107233d557453b9bffaa8198f76a9d9ed2/docs/vendors-aws-blog.png -------------------------------------------------------------------------------- /docs/wireshark-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/flowdog/b58f75107233d557453b9bffaa8198f76a9d9ed2/docs/wireshark-demo.png -------------------------------------------------------------------------------- /examples/account_id_emf/account_id_emf.go: -------------------------------------------------------------------------------- 1 | package account_id_emf 2 | 3 | import ( 4 | "github.com/glassechidna/go-emf/emf" 5 | "github.com/glassechidna/go-emf/emf/unit" 6 | "net/http" 7 | ) 8 | 9 | type AccountIdEmf struct{} 10 | 11 | func (a *AccountIdEmf) OnRequest(req *http.Request) { 12 | auth := req.Header.Get("Authorization") 13 | 14 | parsed, ok := ParseAuthorizationHeader(auth) 15 | if !ok { 16 | return 17 | } 18 | 19 | emf.Namespace = "flowdog" 20 | emf.Emit(emf.MSI{ 21 | "KeyId": parsed.KeyId, 22 | "Date": parsed.Date, 23 | "Region": parsed.Region, 24 | "Service": parsed.Service, 25 | "AccountId": emf.Dimension(parsed.AccountId), 26 | "Requests": emf.Metric(1, unit.Count), 27 | }) 28 | } 29 | 30 | func (a *AccountIdEmf) OnResponse(resp *http.Response) error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /examples/account_id_emf/parse.go: -------------------------------------------------------------------------------- 1 | package account_id_emf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/kenshaw/baseconv" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type ParsedAuthorizationHeader struct { 11 | KeyId string 12 | Date string 13 | Region string 14 | Service string 15 | AccountId string 16 | } 17 | 18 | func ParseAuthorizationHeader(auth string) (ParsedAuthorizationHeader, bool) { 19 | prefix := "AWS4-HMAC-SHA256 Credential=" 20 | if !strings.HasPrefix(auth, prefix) { 21 | return ParsedAuthorizationHeader{}, false 22 | } 23 | 24 | s := strings.Split(strings.TrimPrefix(auth, prefix), "/") 25 | keyId, date, region, service := s[0], s[1], s[2], s[3] 26 | accountId := accountIdFromAccessKeyId(keyId) 27 | return ParsedAuthorizationHeader{ 28 | KeyId: keyId, 29 | Date: date, 30 | Region: region, 31 | Service: service, 32 | AccountId: accountId, 33 | }, true 34 | } 35 | 36 | // from https://awsteele.com/blog/2020/09/26/aws-access-key-format.html 37 | func accountIdFromAccessKeyId(accessKeyId string) string { 38 | base10 := "0123456789" 39 | base32AwsFlavour := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" 40 | 41 | offsetStr, _ := baseconv.Convert("QAAAAAAA", base32AwsFlavour, base10) 42 | offset, _ := strconv.Atoi(offsetStr) 43 | 44 | offsetAccountIdStr, _ := baseconv.Convert(accessKeyId[4:12], base32AwsFlavour, base10) 45 | offsetAccountId, _ := strconv.Atoi(offsetAccountIdStr) 46 | 47 | accountId := 2 * (offsetAccountId - offset) 48 | 49 | if strings.Index(base32AwsFlavour, accessKeyId[12:13]) >= strings.Index(base32AwsFlavour, "Q") { 50 | accountId++ 51 | } 52 | 53 | return fmt.Sprintf("%012d", accountId) 54 | } 55 | -------------------------------------------------------------------------------- /examples/cloudfront_functions/cloudfront_functions.go: -------------------------------------------------------------------------------- 1 | package cloudfront_functions 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/pkg/errors" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "rogchap.com/v8go" 13 | "strings" 14 | ) 15 | 16 | type CloudfrontFunctions struct { 17 | reqV8 *v8go.Context 18 | reqFn *v8go.Function 19 | } 20 | 21 | type cffContextKey string 22 | 23 | const cffContextKeyNewResp = cffContextKey("cffContextKeyNewResp") 24 | 25 | func NewCloudfrontFunctions(onRequest string) (*CloudfrontFunctions, error) { 26 | v8 := v8go.NewContext() 27 | _, err := v8.RunScript(onRequest, "onRequest.js") 28 | if err != nil { 29 | return nil, errors.WithStack(err) 30 | } 31 | 32 | fnv, err := v8.Global().Get("onRequest") 33 | if err != nil { 34 | return nil, errors.WithStack(err) 35 | } 36 | 37 | fn, err := fnv.AsFunction() 38 | if err != nil { 39 | return nil, errors.WithStack(err) 40 | } 41 | 42 | return &CloudfrontFunctions{reqV8: v8, reqFn: fn}, nil 43 | } 44 | 45 | func (c *CloudfrontFunctions) OnRequest(req *http.Request) { 46 | qs := map[string]cffValue{} 47 | for key, values := range req.URL.Query() { 48 | qs[key] = cffValue{Value: values[0]} 49 | } 50 | 51 | hdr := map[string]cffValue{} 52 | for key, values := range req.Header { 53 | hdr[strings.ToLower(key)] = cffValue{Value: values[0]} 54 | } 55 | hdr["host"] = cffValue{Value: req.URL.Host} 56 | 57 | ck := map[string]cffValue{} 58 | 59 | payload := cffEvent{ 60 | Version: "1.0", 61 | Context: cffContext{EventType: "viewer-request"}, 62 | Request: cffRequest{ 63 | Method: req.Method, 64 | Uri: req.URL.Path, 65 | Querystring: qs, 66 | Headers: hdr, 67 | Cookies: ck, 68 | }, 69 | } 70 | 71 | j, _ := json.Marshal(payload) 72 | val, err := v8go.JSONParse(c.reqV8, string(j)) 73 | if err != nil { 74 | fmt.Printf("%+v\n", errors.WithStack(err)) 75 | panic(err) 76 | } 77 | 78 | retval, err := c.reqFn.Call(c.reqV8.Global(), val) 79 | if err != nil { 80 | fmt.Printf("%+v\n", errors.WithStack(err)) 81 | panic(err) 82 | } 83 | 84 | js, err := v8go.JSONStringify(c.reqV8, retval) 85 | if err != nil { 86 | fmt.Printf("%+v\n", errors.WithStack(err)) 87 | panic(err) 88 | } 89 | 90 | newresp := cffResponse{} 91 | _ = json.Unmarshal([]byte(js), &newresp) 92 | 93 | if newresp.StatusCode > 0 { 94 | // TODO: this doesn't actually stop the original request from being fired. 95 | fmt.Println("editing response instead") 96 | ctx := context.WithValue(req.Context(), cffContextKeyNewResp, &newresp) 97 | reqctx := req.WithContext(ctx) 98 | *req = *reqctx 99 | } else { 100 | err = c.modifyRequest(req, js) 101 | if err != nil { 102 | fmt.Printf("%+v\n", err) 103 | panic(err) 104 | } 105 | } 106 | } 107 | 108 | func (c *CloudfrontFunctions) modifyRequest(req *http.Request, js string) error { 109 | newreq := cffRequest{} 110 | err := json.Unmarshal([]byte(js), &newreq) 111 | if err != nil { 112 | return errors.WithStack(err) 113 | } 114 | 115 | req.Method = newreq.Method 116 | req.Header = http.Header{} 117 | for key, value := range newreq.Headers { 118 | req.Header.Set(key, value.Value) 119 | } 120 | 121 | newhost := newreq.Headers["host"].Value 122 | req.URL.Host = newhost 123 | req.Host = req.URL.Host 124 | req.URL.Path = newreq.Uri 125 | 126 | q := url.Values{} 127 | for key, value := range newreq.Querystring { 128 | q.Set(key, value.Value) 129 | } 130 | req.URL.RawQuery = q.Encode() 131 | 132 | // TODO: cookies 133 | // TODO: body 134 | return nil 135 | } 136 | 137 | func (c *CloudfrontFunctions) OnResponse(resp *http.Response) error { 138 | newresp, ok := resp.Request.Context().Value(cffContextKeyNewResp).(*cffResponse) 139 | if !ok { 140 | return nil 141 | } 142 | 143 | fmt.Println("returning edited response") 144 | resp.StatusCode = newresp.StatusCode 145 | resp.Status = fmt.Sprintf("%d %s", newresp.StatusCode, newresp.StatusDescription) 146 | resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) 147 | 148 | resp.Header = http.Header{} 149 | for key, value := range newresp.Headers { 150 | resp.Header.Set(key, value.Value) 151 | } 152 | 153 | return nil 154 | } 155 | 156 | type cffContext struct { 157 | DistributionDomainName string `json:"distributionDomainName"` 158 | DistributionId string `json:"distributionId"` 159 | EventType string `json:"eventType"` 160 | RequestId string `json:"requestId"` 161 | } 162 | 163 | type cffValue struct { 164 | Value string `json:"value"` 165 | } 166 | 167 | type cffRequest struct { 168 | Method string `json:"method"` 169 | Uri string `json:"uri"` 170 | Querystring map[string]cffValue `json:"querystring"` 171 | Headers map[string]cffValue `json:"headers"` 172 | Cookies map[string]cffValue `json:"cookies"` 173 | } 174 | 175 | type cffSetCookie struct { 176 | Value string `json:"value"` 177 | Attributes string `json:"attributes"` 178 | } 179 | 180 | type cffResponse struct { 181 | StatusCode int `json:"statusCode"` 182 | StatusDescription string `json:"statusDescription"` 183 | Headers map[string]cffValue `json:"headers"` 184 | Cookies map[string]cffSetCookie `json:"cookies"` 185 | } 186 | 187 | type cffEvent struct { 188 | Version string `json:"version"` 189 | Context cffContext `json:"context"` 190 | Request cffRequest `json:"request,omitempty"` 191 | Response cffResponse `json:"response,omitempty"` 192 | Viewer struct { 193 | Ip string `json:"ip"` 194 | } `json:"viewer"` 195 | } 196 | -------------------------------------------------------------------------------- /examples/cloudfront_functions/cloudfront_functions_test.go: -------------------------------------------------------------------------------- 1 | package cloudfront_functions 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestCloudfrontFunctions(t *testing.T) { 10 | cff, err := NewCloudfrontFunctions(` 11 | function onRequest(event) { 12 | return JSON.stringify(event.request); 13 | } 14 | `) 15 | require.NoError(t, err) 16 | 17 | req, _ := http.NewRequest("GET", "https://google.com/abc?def=ghi", nil) 18 | req.Header.Set("Authorization", "monkey") 19 | cff.OnRequest(req) 20 | } 21 | -------------------------------------------------------------------------------- /examples/cloudfront_functions/rick.go: -------------------------------------------------------------------------------- 1 | package cloudfront_functions 2 | 3 | import ( 4 | "github.com/aidansteele/flowdog/gwlb" 5 | "io/ioutil" 6 | ) 7 | 8 | func NewRickroll() gwlb.Interceptor { 9 | script, _ := ioutil.ReadFile("rick.js") 10 | cff, _ := NewCloudfrontFunctions(string(script)) 11 | return cff 12 | } 13 | -------------------------------------------------------------------------------- /examples/cloudfront_functions/rick.js: -------------------------------------------------------------------------------- 1 | function onRequest(event) { 2 | const r = event.request; 3 | if (r.headers.host.value !== "www.youtube.com") { 4 | return r; 5 | } 6 | 7 | const onlyVideoOnYoutube = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; 8 | const referer = r.headers.referer; 9 | if (referer && referer.value === onlyVideoOnYoutube) { 10 | return r; 11 | } 12 | 13 | if (r.uri === "/watch" && r.querystring.v.value === "dQw4w9WgXcQ") { 14 | return r; 15 | } 16 | 17 | return { 18 | statusCode: 302, 19 | statusDescription: 'Found', 20 | headers: { 21 | location: { value: onlyVideoOnYoutube } 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /examples/flowdogshark/flowdogshark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aidansteele/flowdog/gwlb/shark" 7 | "github.com/google/gopacket" 8 | "github.com/google/gopacket/layers" 9 | "github.com/google/gopacket/pcapgo" 10 | "github.com/kor44/extcap" 11 | "github.com/pkg/errors" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials/insecure" 14 | "io" 15 | "os" 16 | "strings" 17 | ) 18 | 19 | func main() { 20 | // TODO: open upstream github issue about capture filter validation not working 21 | // note that program is invoked for every keystroke in capture filter textbox 22 | if len(os.Args) > 1 && os.Args[1] == "--extcap-capture-filter" { 23 | _, err := shark.FilterVM(os.Args[2]) 24 | if err == nil { 25 | os.Exit(0) 26 | } 27 | } 28 | 29 | // TODO: open upstream issue about this unrecognised flag warning 30 | versionIdx := -1 31 | for idx, arg := range os.Args { 32 | if strings.HasPrefix(arg, "--extcap-version") { 33 | versionIdx = idx 34 | } 35 | } 36 | if versionIdx > 0 { 37 | os.Args = append(os.Args[:versionIdx], os.Args[versionIdx+1:]...) 38 | } 39 | 40 | app := extcap.App{ 41 | Usage: "flowdogshark", 42 | HelpPage: "flowdogshark attaches to flowdog-managed AWS GWLB appliances for VPC-wide packet capture", 43 | GetInterfaces: getAllInterfaces, 44 | GetDLT: getDLT, 45 | StartCapture: startCapture, 46 | GetAllConfigOptions: func() []extcap.ConfigOption { 47 | return []extcap.ConfigOption{} 48 | }, 49 | GetConfigOptions: func(iface string) ([]extcap.ConfigOption, error) { 50 | return []extcap.ConfigOption{}, nil 51 | }, 52 | } 53 | 54 | app.Run(os.Args) 55 | } 56 | 57 | func getAllInterfaces() ([]extcap.CaptureInterface, error) { 58 | return []extcap.CaptureInterface{ 59 | { 60 | Value: "vpce-08adcae6a2example", 61 | Display: "flowdogshark: display", 62 | }, 63 | }, nil 64 | } 65 | 66 | func getDLT(iface string) (extcap.DLT, error) { 67 | return extcap.DLT{ 68 | Number: int(layers.LinkTypeRaw), 69 | Name: "LINKTYPE_RAW", 70 | Display: "dlt display?", 71 | }, nil 72 | } 73 | 74 | func startCapture(iface string, pipe io.WriteCloser, filter string, opts map[string]interface{}) error { 75 | defer pipe.Close() 76 | w, err := pcapgo.NewNgWriter(pipe, layers.LinkTypeRaw) 77 | if err != nil { 78 | return errors.WithStack(err) 79 | } 80 | 81 | ctx := context.Background() 82 | conn, err := grpc.DialContext( 83 | ctx, 84 | "127.0.0.1:7081", 85 | grpc.WithTransportCredentials(insecure.NewCredentials()), 86 | ) 87 | if err != nil { 88 | return errors.WithStack(err) 89 | } 90 | 91 | client := shark.NewVpcsharkClient(conn) 92 | cc, err := client.GetPackets(ctx, &shark.GetPacketsInput{ 93 | Filter: filter, 94 | PacketType: shark.PacketType_PRE, 95 | }) 96 | if err != nil { 97 | fmt.Printf("%+v\n", err) 98 | panic(err) 99 | } 100 | 101 | for { 102 | msg, err := cc.Recv() 103 | if err != nil { 104 | return errors.WithStack(err) 105 | } 106 | 107 | if msg.SslKeyLog != nil { 108 | err = w.WriteDecryptionSecrets(pcapgo.NgDecryptionSecrets{ 109 | Type: pcapgo.NgDecryptionSecretTypeTLSKeyLog, 110 | Data: msg.SslKeyLog, 111 | }) 112 | if err != nil { 113 | return errors.WithStack(err) 114 | } 115 | } 116 | 117 | if msg.Payload != nil { 118 | gpkt := gopacket.NewPacket(msg.Payload, layers.LayerTypeGeneve, gopacket.Default) 119 | geneve := gpkt.Layer(layers.LayerTypeGeneve).(*layers.Geneve) 120 | payload := geneve.LayerPayload() 121 | 122 | err = w.WritePacket(gopacket.CaptureInfo{ 123 | Timestamp: msg.Time.AsTime(), 124 | CaptureLength: len(payload), 125 | Length: len(payload), 126 | InterfaceIndex: 0, 127 | }, payload) 128 | if err != nil { 129 | return errors.WithStack(err) 130 | } 131 | } 132 | 133 | err = w.Flush() 134 | if err != nil { 135 | return errors.WithStack(err) 136 | } 137 | } 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /examples/geneve_headers/geneve_headers.go: -------------------------------------------------------------------------------- 1 | package geneve_headers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aidansteele/flowdog/gwlb" 6 | "net/http" 7 | ) 8 | 9 | type GeneveHeaders struct{} 10 | 11 | func (g *GeneveHeaders) OnRequest(req *http.Request) { 12 | opts := gwlb.GeneveOptionsFromContext(req.Context()) 13 | addHeaders(req.Header, opts) 14 | } 15 | 16 | func (g *GeneveHeaders) OnResponse(resp *http.Response) error { 17 | opts := gwlb.GeneveOptionsFromContext(resp.Request.Context()) 18 | addHeaders(resp.Header, opts) 19 | return nil 20 | } 21 | 22 | func addHeaders(header http.Header, opts gwlb.AwsGeneveOptions) { 23 | header.Set("X-Gwlb-Vpc-Endpoint-Id", fmt.Sprintf("vpce-0%016x", opts.VpcEndpointId)) 24 | header.Set("X-Gwlb-Attachment-Id", fmt.Sprintf("%016x", opts.AttachmentId)) 25 | header.Set("X-Gwlb-Flow-Cookie", fmt.Sprintf("%08x", opts.FlowCookie)) 26 | } 27 | -------------------------------------------------------------------------------- /examples/lambda_acceptor/cfn.yml: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | Function: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | CodeUri: ./lambda/bootstrap 8 | Architectures: [arm64] 9 | AutoPublishAlias: live 10 | Runtime: provided.al2 11 | Handler: unused 12 | MemorySize: 512 13 | Layers: 14 | - !Sub arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension-Arm64:1 15 | 16 | Outputs: 17 | Function: 18 | Value: !Ref Function.Version 19 | -------------------------------------------------------------------------------- /examples/lambda_acceptor/enricher.go: -------------------------------------------------------------------------------- 1 | package lambda_acceptor 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/ec2" 5 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type Enricher struct { 10 | // TODO: obviously this should update live in response to eventbridge events 11 | cache map[string]*ec2.Instance 12 | } 13 | 14 | func NewEnricher(api ec2iface.EC2API) (*Enricher, error) { 15 | m := map[string]*ec2.Instance{} 16 | 17 | err := api.DescribeInstancesPages(&ec2.DescribeInstancesInput{}, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool { 18 | for _, r := range page.Reservations { 19 | for _, i := range r.Instances { 20 | instance := i 21 | for _, iface := range i.NetworkInterfaces { 22 | m[*iface.PrivateIpAddress] = instance 23 | } 24 | } 25 | } 26 | return !lastPage 27 | }) 28 | if err != nil { 29 | return nil, errors.WithStack(err) 30 | } 31 | 32 | return &Enricher{cache: m}, nil 33 | } 34 | 35 | func (e *Enricher) InstanceByIp(ip string) *ec2.Instance { 36 | return e.cache[ip] 37 | } 38 | -------------------------------------------------------------------------------- /examples/lambda_acceptor/lambda/lambda.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/aidansteele/flowdog/examples/lambda_acceptor" 8 | "github.com/aws/aws-lambda-go/lambda" 9 | ) 10 | 11 | func main() { 12 | lambda.Start(handle) 13 | } 14 | 15 | func handle(ctx context.Context, input *lambda_acceptor.AcceptorInput) (*lambda_acceptor.AcceptorOutput, error) { 16 | j, _ := json.Marshal(input) 17 | fmt.Println(string(j)) 18 | 19 | output := &lambda_acceptor.AcceptorOutput{Accept: true} 20 | j, _ = json.Marshal(output) 21 | fmt.Println(string(j)) 22 | return output, nil 23 | } 24 | -------------------------------------------------------------------------------- /examples/lambda_acceptor/lambda_acceptor.go: -------------------------------------------------------------------------------- 1 | package lambda_acceptor 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/aidansteele/flowdog/gwlb" 8 | 9 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 10 | "github.com/aws/aws-sdk-go/service/lambda" 11 | "github.com/aws/aws-sdk-go/service/lambda/lambdaiface" 12 | "github.com/google/gopacket" 13 | "github.com/google/gopacket/layers" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type LambdaAcceptor struct { 18 | lambda lambdaiface.LambdaAPI 19 | function string 20 | enricher *Enricher 21 | } 22 | 23 | func New(lambda lambdaiface.LambdaAPI, function string, ec2 ec2iface.EC2API) (*LambdaAcceptor, error) { 24 | enricher, err := NewEnricher(ec2) 25 | if err != nil { 26 | return nil, errors.WithStack(err) 27 | } 28 | 29 | return &LambdaAcceptor{ 30 | lambda: lambda, 31 | function: function, 32 | enricher: enricher, 33 | }, nil 34 | } 35 | 36 | type AcceptorInput struct { 37 | GeneveOptions GeneveOptions 38 | NetworkProtocol string 39 | TransportProtocol string 40 | Source Endpoint 41 | Destination Endpoint 42 | } 43 | 44 | type GeneveOptions struct { 45 | VpcEndpointId string 46 | FlowCookie string 47 | AttachmentId string `json:"AttachmentId,omitempty"` 48 | } 49 | 50 | type Endpoint struct { 51 | IpAddress string 52 | Port int `json:"Port,omitempty"` 53 | EC2 *Ec2Endpoint `json:"EC2,omitempty"` 54 | } 55 | 56 | type Ec2Endpoint struct { 57 | InstanceId string 58 | AccountId string 59 | VpcId string 60 | SubnetId string 61 | Tags map[string]string 62 | } 63 | 64 | type AcceptorOutput struct { 65 | Accept bool 66 | } 67 | 68 | func (a *LambdaAcceptor) AcceptFlow(ctx context.Context, pkt gopacket.Packet, opts gwlb.AwsGeneveOptions) bool { 69 | sourceIp := "" 70 | destIp := "" 71 | networkProto := "" 72 | 73 | switch network := pkt.NetworkLayer().(type) { 74 | case *layers.IPv4: 75 | networkProto = "IPv4" 76 | sourceIp = network.SrcIP.String() 77 | destIp = network.DstIP.String() 78 | case *layers.IPv6: 79 | networkProto = "IPv6" 80 | sourceIp = network.SrcIP.String() 81 | destIp = network.DstIP.String() 82 | } 83 | 84 | sourcePort := 0 85 | destPort := 0 86 | transportProto := "" 87 | 88 | switch transport := pkt.TransportLayer().(type) { 89 | case *layers.TCP: 90 | transportProto = "TCP" 91 | sourcePort = int(transport.SrcPort) 92 | destPort = int(transport.DstPort) 93 | case *layers.UDP: 94 | transportProto = "UDP" 95 | sourcePort = int(transport.SrcPort) 96 | destPort = int(transport.DstPort) 97 | } 98 | 99 | attachmentId := "" 100 | if opts.AttachmentId > 0 { 101 | attachmentId = fmt.Sprintf("%016x", opts.AttachmentId) 102 | } 103 | 104 | inputPayload, _ := json.Marshal(AcceptorInput{ 105 | GeneveOptions: GeneveOptions{ 106 | VpcEndpointId: fmt.Sprintf("vpce-0%016x", opts.VpcEndpointId), 107 | FlowCookie: fmt.Sprintf("%08x", opts.FlowCookie), 108 | AttachmentId: attachmentId, 109 | }, 110 | NetworkProtocol: networkProto, 111 | TransportProtocol: transportProto, 112 | Source: Endpoint{ 113 | IpAddress: sourceIp, 114 | Port: sourcePort, 115 | EC2: a.ec2Endpoint(sourceIp), 116 | }, 117 | Destination: Endpoint{ 118 | IpAddress: destIp, 119 | Port: destPort, 120 | EC2: a.ec2Endpoint(destIp), 121 | }, 122 | }) 123 | 124 | invoke, err := a.lambda.InvokeWithContext(ctx, &lambda.InvokeInput{ 125 | FunctionName: &a.function, 126 | Payload: inputPayload, 127 | }) 128 | if err != nil { 129 | fmt.Printf("lambda acceptor err %+v\n", err) 130 | return false 131 | } 132 | 133 | output := AcceptorOutput{} 134 | err = json.Unmarshal(invoke.Payload, &output) 135 | if err != nil { 136 | fmt.Printf("parsing lambda acceptor output err %+v\n", err) 137 | return false 138 | } 139 | 140 | return output.Accept 141 | } 142 | 143 | // TODO: this enriched info should be part of the ctx 144 | // earlier on so it can be used by interceptors etc 145 | func (a *LambdaAcceptor) ec2Endpoint(ip string) *Ec2Endpoint { 146 | instance := a.enricher.InstanceByIp(ip) 147 | if instance == nil { 148 | return nil 149 | } 150 | 151 | iface := instance.NetworkInterfaces[0] 152 | tags := map[string]string{} 153 | for _, tag := range instance.Tags { 154 | tags[*tag.Key] = *tag.Value 155 | } 156 | 157 | return &Ec2Endpoint{ 158 | InstanceId: *instance.InstanceId, 159 | AccountId: *iface.OwnerId, 160 | VpcId: *iface.VpcId, 161 | SubnetId: *iface.SubnetId, 162 | Tags: tags, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /examples/sts_rickroll/sts_rickroll.go: -------------------------------------------------------------------------------- 1 | package sts_rickroll 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/pkg/errors" 7 | "io/ioutil" 8 | "net/http" 9 | "regexp" 10 | ) 11 | 12 | type StsRickroll struct{} 13 | 14 | var stsRickrollRegexp = regexp.MustCompile(`([^<]+)`) 15 | 16 | func (s *StsRickroll) OnRequest(req *http.Request) {} 17 | 18 | func (s *StsRickroll) OnResponse(resp *http.Response) error { 19 | if resp.Request.Host != "sts.amazonaws.com" { 20 | return nil 21 | } 22 | 23 | body, err := ioutil.ReadAll(resp.Body) 24 | if err != nil { 25 | return errors.WithStack(err) 26 | } 27 | defer resp.Body.Close() 28 | 29 | body = stsRickrollRegexp.ReplaceAll(body, []byte(`Never gonna give you up, never gonna let you down.`)) 30 | 31 | resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(body))) 32 | resp.Body = ioutil.NopCloser(bytes.NewReader(body)) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /examples/upsidedown/upsidedown.go: -------------------------------------------------------------------------------- 1 | package upsidedown 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/aidansteele/flowdog/gwlb" 7 | "github.com/disintegration/imaging" 8 | "github.com/pkg/errors" 9 | _ "image/png" 10 | "io/ioutil" 11 | "net/http" 12 | "strings" 13 | ) 14 | 15 | type interceptor struct{} 16 | 17 | // all credit to http://www.ex-parrot.com/pete/upside-down-ternet.html 18 | 19 | func UpsideDown() gwlb.Interceptor { 20 | return &interceptor{} 21 | } 22 | 23 | func (i *interceptor) OnRequest(req *http.Request) { 24 | // content ranges make this prank more work 25 | req.Header.Del("Range") 26 | req.Header.Del("If-Range") 27 | } 28 | 29 | func (i *interceptor) OnResponse(resp *http.Response) error { 30 | resp.Header.Del("Accept-Ranges") 31 | 32 | contentType := resp.Header.Get("Content-Type") 33 | if !strings.HasPrefix(contentType, "image/") { 34 | return nil 35 | } 36 | 37 | originalBody := resp.Body 38 | defer originalBody.Close() 39 | 40 | img, err := imaging.Decode(originalBody) 41 | if err != nil { 42 | return errors.WithStack(err) 43 | } 44 | 45 | // perform the.. "magic" 46 | img = imaging.Rotate180(img) 47 | img = imaging.Blur(img, 1.5) 48 | 49 | newBody := &bytes.Buffer{} 50 | resp.Body = ioutil.NopCloser(newBody) 51 | 52 | // i'm lazy so lets make everything a png 53 | err = imaging.Encode(newBody, img, imaging.PNG) 54 | if err != nil { 55 | return errors.WithStack(err) 56 | } 57 | 58 | resp.Header.Set("Content-Type", "image/png") 59 | resp.Header.Set("Content-Length", fmt.Sprintf("%d", newBody.Len())) 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /examples/webfirehose/webfirehose.go: -------------------------------------------------------------------------------- 1 | package webfirehose 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/aidansteele/flowdog/examples/account_id_emf" 8 | "github.com/aidansteele/flowdog/examples/lambda_acceptor" 9 | "github.com/aidansteele/flowdog/gwlb" 10 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 11 | "github.com/aws/aws-sdk-go/service/firehose" 12 | "github.com/aws/aws-sdk-go/service/firehose/firehoseiface" 13 | "github.com/pkg/errors" 14 | "golang.org/x/net/publicsuffix" 15 | "net" 16 | "net/http" 17 | "time" 18 | ) 19 | 20 | type requestRecord struct { 21 | TLS *requestRecordTls `json:"TLS,omitempty"` 22 | AWS *requestRecordAws `json:"AWS,omitempty"` 23 | Geneve requestRecordGeneve 24 | 25 | Time time.Time 26 | Method string 27 | Host string 28 | ETldPlusOne string 29 | URL string 30 | RequestHeaders map[string]string 31 | Client lambda_acceptor.Endpoint 32 | 33 | StatusCode int 34 | StatusMessage string 35 | ResponseHeaders map[string]string 36 | Server lambda_acceptor.Endpoint 37 | } 38 | 39 | type requestRecordAws struct { 40 | AuthorizationHeader account_id_emf.ParsedAuthorizationHeader 41 | } 42 | 43 | type requestRecordTls struct { 44 | Version uint16 45 | CipherSuite uint16 46 | } 47 | 48 | type requestRecordGeneve struct { 49 | VpcEndpointId string 50 | AttachmentId string 51 | FlowCookie string 52 | } 53 | 54 | type WebFirehose struct { 55 | enricher *lambda_acceptor.Enricher 56 | firehose firehoseiface.FirehoseAPI 57 | stream string 58 | entries chan []byte 59 | } 60 | 61 | func New(firehose firehoseiface.FirehoseAPI, ec2 ec2iface.EC2API, stream string) *WebFirehose { 62 | enricher, err := lambda_acceptor.NewEnricher(ec2) 63 | if err != nil { 64 | fmt.Printf("%+v\n", err) 65 | panic(err) 66 | } 67 | 68 | return &WebFirehose{ 69 | firehose: firehose, 70 | stream: stream, 71 | enricher: enricher, 72 | entries: make(chan []byte), 73 | } 74 | } 75 | 76 | func (wf *WebFirehose) Run(ctx context.Context) error { 77 | batch := []*firehose.Record{} 78 | size := 0 79 | ticker := time.NewTicker(time.Second) 80 | 81 | for { 82 | select { 83 | case <-ctx.Done(): 84 | return ctx.Err() 85 | case <-ticker.C: 86 | if len(batch) == 0 { 87 | continue 88 | } 89 | 90 | // TODO: check batch count, combined batch size, etc 91 | _, err := wf.firehose.PutRecordBatchWithContext(ctx, &firehose.PutRecordBatchInput{ 92 | DeliveryStreamName: &wf.stream, 93 | Records: batch, 94 | }) 95 | if err != nil { 96 | return errors.WithStack(err) 97 | } 98 | 99 | fmt.Printf("firehose dump: count=%d bytes=%d\n", len(batch), size) 100 | size = 0 101 | batch = nil 102 | case entry := <-wf.entries: 103 | fmt.Printf(string(entry)) 104 | record := &firehose.Record{Data: entry} 105 | batch = append(batch, record) 106 | size += len(record.Data) 107 | } 108 | } 109 | } 110 | 111 | func (wf *WebFirehose) OnRequest(req *http.Request) { 112 | } 113 | 114 | func (wf *WebFirehose) OnResponse(resp *http.Response) error { 115 | ctx := resp.Request.Context() 116 | geneve := gwlb.GeneveOptionsFromContext(ctx) 117 | 118 | source, dest := gwlb.AddrsFromContext(ctx) 119 | var client, server *net.TCPAddr 120 | 121 | if source.Port == 443 || source.Port == 80 { 122 | server = source 123 | client = dest 124 | } else { 125 | server = dest 126 | client = source 127 | } 128 | 129 | var tlsrr *requestRecordTls 130 | if server.Port == 443 { 131 | tlsrr = &requestRecordTls{ 132 | Version: resp.Request.TLS.Version, 133 | CipherSuite: resp.Request.TLS.CipherSuite, 134 | } 135 | } 136 | 137 | var awsrr *requestRecordAws 138 | if parsed, ok := account_id_emf.ParseAuthorizationHeader(resp.Request.Header.Get("Authorization")); ok { 139 | awsrr = &requestRecordAws{ 140 | AuthorizationHeader: parsed, 141 | } 142 | } 143 | 144 | host := resp.Request.URL.Host 145 | etldp1, _ := publicsuffix.EffectiveTLDPlusOne(host) 146 | 147 | rr := requestRecord{ 148 | TLS: tlsrr, 149 | Geneve: requestRecordGeneve{ 150 | VpcEndpointId: fmt.Sprintf("vpce-0%016x", geneve.VpcEndpointId), 151 | AttachmentId: fmt.Sprintf("%016x", geneve.AttachmentId), 152 | FlowCookie: fmt.Sprintf("%08x", geneve.FlowCookie), 153 | }, 154 | AWS: awsrr, 155 | Time: time.Now(), 156 | Method: resp.Request.Method, 157 | Host: host, 158 | ETldPlusOne: etldp1, 159 | URL: resp.Request.URL.String(), 160 | RequestHeaders: flattenHeaders(resp.Request.Header), 161 | Client: lambda_acceptor.Endpoint{ 162 | IpAddress: client.IP.String(), 163 | Port: client.Port, 164 | EC2: wf.ec2Endpoint(client.IP.String()), 165 | }, 166 | 167 | StatusCode: resp.StatusCode, 168 | StatusMessage: resp.Status, 169 | ResponseHeaders: flattenHeaders(resp.Header), 170 | Server: lambda_acceptor.Endpoint{ 171 | IpAddress: server.IP.String(), 172 | Port: server.Port, 173 | EC2: wf.ec2Endpoint(server.IP.String()), 174 | }, 175 | } 176 | 177 | rrj, _ := json.Marshal(rr) 178 | wf.entries <- append(rrj, '\n') 179 | 180 | return nil 181 | } 182 | 183 | // TODO: this enriched info should be part of the ctx 184 | // earlier on so it can be used by interceptors etc 185 | func (wf *WebFirehose) ec2Endpoint(ip string) *lambda_acceptor.Ec2Endpoint { 186 | instance := wf.enricher.InstanceByIp(ip) 187 | if instance == nil { 188 | return nil 189 | } 190 | 191 | iface := instance.NetworkInterfaces[0] 192 | tags := map[string]string{} 193 | for _, tag := range instance.Tags { 194 | tags[*tag.Key] = *tag.Value 195 | } 196 | 197 | return &lambda_acceptor.Ec2Endpoint{ 198 | InstanceId: *instance.InstanceId, 199 | AccountId: *iface.OwnerId, 200 | VpcId: *iface.VpcId, 201 | SubnetId: *iface.SubnetId, 202 | Tags: tags, 203 | } 204 | } 205 | 206 | func flattenHeaders(head http.Header) map[string]string { 207 | flat := map[string]string{} 208 | 209 | for key, vals := range head { 210 | flat[key] = vals[0] 211 | } 212 | 213 | return flat 214 | } 215 | -------------------------------------------------------------------------------- /examples/webfirehose/webfirehose.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Firehose: 3 | Type: AWS::KinesisFirehose::DeliveryStream 4 | Properties: 5 | DeliveryStreamType: DirectPut 6 | ExtendedS3DestinationConfiguration: 7 | BucketARN: !Sub arn:aws:s3:::${Bucket} 8 | RoleARN: !GetAtt Role.Arn 9 | Prefix: output/!{timestamp:yyyy/MM/dd}/!{partitionKeyFromQuery:etldp1}/ 10 | ErrorOutputPrefix: errors/!{firehose:error-output-type}/!{timestamp:yyyy/MM/dd}/ 11 | CompressionFormat: GZIP 12 | BufferingHints: 13 | IntervalInSeconds: 60 14 | SizeInMBs: 128 15 | DynamicPartitioningConfiguration: 16 | Enabled: true 17 | RetryOptions: 18 | DurationInSeconds: 300 19 | ProcessingConfiguration: 20 | Enabled: true 21 | Processors: 22 | - Type: MetadataExtraction 23 | Parameters: 24 | - ParameterName: MetadataExtractionQuery 25 | ParameterValue: "{etldp1:.ETldPlusOne}" 26 | - ParameterName: JsonParsingEngine 27 | ParameterValue: JQ-1.6 28 | 29 | Bucket: 30 | Type: AWS::S3::Bucket 31 | DeletionPolicy: Retain 32 | 33 | Role: 34 | Type: AWS::IAM::Role 35 | Properties: 36 | AssumeRolePolicyDocument: 37 | Version: 2012-10-17 38 | Statement: 39 | - Effect: Allow 40 | Principal: 41 | Service: firehose.amazonaws.com 42 | Action: sts:AssumeRole 43 | Condition: 44 | StringEquals: 45 | sts:ExternalId: !Ref AWS::AccountId 46 | Policies: 47 | - PolicyName: Firehose 48 | PolicyDocument: 49 | Version: 2012-10-17 50 | Statement: 51 | - Effect: Allow 52 | Action: 53 | - s3:AbortMultipartUpload 54 | - s3:GetBucketLocation 55 | - s3:GetObject 56 | - s3:ListBucket 57 | - s3:ListBucketMultipartUploads 58 | - s3:PutObject 59 | Resource: 60 | - !Sub arn:aws:s3:::${Bucket} 61 | - !Sub arn:aws:s3:::${Bucket}/* 62 | 63 | Outputs: 64 | Firehose: 65 | Value: !Ref Firehose 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aidansteele/flowdog 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.28.0 7 | github.com/aws/aws-sdk-go v1.42.31 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/disintegration/imaging v1.6.2 10 | github.com/glassechidna/go-emf v0.0.0-20220102031255-2c11928b55f0 11 | github.com/google/btree v1.0.1 // indirect 12 | github.com/google/gopacket v1.1.19 13 | github.com/google/pprof v0.0.0-20220128192902-513e8ac6eea1 // indirect 14 | github.com/gorilla/websocket v1.4.2 15 | github.com/kenshaw/baseconv v0.1.1 16 | github.com/kor44/extcap v0.0.0-20201215145008-71f6cf07bb46 17 | github.com/pkg/errors v0.9.1 18 | github.com/stretchr/testify v1.7.0 19 | github.com/tidwall/spinlock v0.1.0 20 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect 21 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 22 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 23 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect 24 | golang.org/x/text v0.3.7 // indirect 25 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect 26 | google.golang.org/genproto v0.0.0-20220112215332-a9c7c0acf9f2 // indirect 27 | google.golang.org/grpc v1.43.0 28 | google.golang.org/protobuf v1.27.1 29 | gopkg.in/DataDog/dd-trace-go.v1 v1.36.0 30 | inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c 31 | rogchap.com/v8go v0.7.0 32 | ) 33 | 34 | replace ( 35 | github.com/google/gopacket v1.1.19 => ./forks/gopacket 36 | rogchap.com/v8go v0.7.0 => github.com/rogchap/v8go v0.7.1-0.20220106173329-ede7cee433be 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw= 5 | github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw= 6 | github.com/DataDog/datadog-go v4.8.2+incompatible h1:qbcKSx29aBLD+5QLvlQZlGmRMF/FfGqFLFev/1TDzRo= 7 | github.com/DataDog/datadog-go v4.8.2+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 8 | github.com/DataDog/datadog-go/v5 v5.0.2 h1:UFtEe7662/Qojxkw1d6SboAeA0CPI3naKhVASwFn+04= 9 | github.com/DataDog/datadog-go/v5 v5.0.2/go.mod h1:ZI9JFB4ewXbw1sBnF4sxsR2k1H3xjV+PUAOUsHvKpcU= 10 | github.com/DataDog/gostackparse v0.5.0 h1:jb72P6GFHPHz2W0onsN51cS3FkaMDcjb0QzgxxA4gDk= 11 | github.com/DataDog/gostackparse v0.5.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= 12 | github.com/DataDog/sketches-go v1.0.0 h1:chm5KSXO7kO+ywGWJ0Zs6tdmWU8PBXSbywFVciL6BG4= 13 | github.com/DataDog/sketches-go v1.0.0/go.mod h1:O+XkJHWk9w4hDwY2ZUDU31ZC9sNYlYo8DiFsxjYeo1k= 14 | github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= 15 | github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= 16 | github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= 17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 18 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 19 | github.com/aws/aws-lambda-go v1.28.0 h1:fZiik1PZqW2IyAN4rj+Y0UBaO1IDFlsNo9Zz/XnArK4= 20 | github.com/aws/aws-lambda-go v1.28.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 21 | github.com/aws/aws-sdk-go v1.42.31 h1:tSv/YzjrFlbSqWmov9quBxrSNXLPUjJI7nPEB57S1+M= 22 | github.com/aws/aws-sdk-go v1.42.31/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= 23 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 24 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 25 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 26 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 27 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 28 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 29 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 30 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 31 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 32 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 33 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 34 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 35 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 36 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 37 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 38 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 39 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 40 | github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 41 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 42 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 44 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= 46 | github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= 47 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 48 | github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= 49 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 50 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 51 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 52 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 54 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 55 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 56 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 57 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 58 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 59 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 60 | github.com/glassechidna/go-emf v0.0.0-20220102031255-2c11928b55f0 h1:JJjK59ANTLUR+tHran8Q710jESoculect7MOIgwmx+I= 61 | github.com/glassechidna/go-emf v0.0.0-20220102031255-2c11928b55f0/go.mod h1:Uj0udAoNiziSRqHaOn0SjnlKmfB1Owu6p9B5oFEzwhE= 62 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 63 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 64 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 65 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 68 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 69 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 70 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 71 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 72 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 73 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 74 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 75 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 76 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 77 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 78 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 79 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 80 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 81 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 82 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 83 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 84 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 85 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 86 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 87 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 88 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 89 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 90 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 91 | github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 92 | github.com/google/pprof v0.0.0-20220128192902-513e8ac6eea1 h1:fBq9NM3cpLvsvc+RkweQROGEeYx7QEk35Htz7bAmyuM= 93 | github.com/google/pprof v0.0.0-20220128192902-513e8ac6eea1/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= 94 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 95 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 96 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 97 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 98 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 99 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 100 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 101 | github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 102 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 103 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 104 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 105 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 106 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 107 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 108 | github.com/kenshaw/baseconv v0.1.1 h1:oAu/C7ipUT2PqT9DT0mZDGDg4URIglizZMjPv9oCu0E= 109 | github.com/kenshaw/baseconv v0.1.1/go.mod h1:yy9zGmnnR6vgOxOQb702nVdAG30JhyYZpj/5/m0siRI= 110 | github.com/kor44/extcap v0.0.0-20201215145008-71f6cf07bb46 h1:JYYyCLuPEImJw4upnIY6SPSJUZv2fjd+ovLK9DbK2Og= 111 | github.com/kor44/extcap v0.0.0-20201215145008-71f6cf07bb46/go.mod h1:H7/WxrChVI6uldeNImZeE6qDy/wuxXD2A//yL5ZernI= 112 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 113 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 114 | github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= 115 | github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 116 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 117 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 118 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 119 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 120 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 121 | github.com/rogchap/v8go v0.7.1-0.20220106173329-ede7cee433be h1:4SlkuW7JbBKpqkyCzecWXRz3QkDQB8hSMgqF0KaUTzc= 122 | github.com/rogchap/v8go v0.7.1-0.20220106173329-ede7cee433be/go.mod h1:MxgP3pL2MW4dpme/72QRs8sgNMmM0pRc8DPhcuLWPAs= 123 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 124 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 125 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 126 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 127 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 128 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 129 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 130 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 131 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 132 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 133 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 134 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 135 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 136 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 137 | github.com/tidwall/spinlock v0.1.0 h1:McCloMBXOyGiQWG1D5kHOzSA50u1UV/g3P2SAiuvnPA= 138 | github.com/tidwall/spinlock v0.1.0/go.mod h1:tbnEyV8RKQ/S4PpqY6bxHz1TWomMPQ+YkJLcP2DByiA= 139 | github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= 140 | github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 141 | github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= 142 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 143 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 144 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 145 | github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 146 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 147 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 148 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 149 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 150 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 151 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 152 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= 153 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 154 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 155 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 156 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 157 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 158 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 159 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 160 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 163 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 164 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 165 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 166 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 167 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 168 | golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 169 | golang.org/x/net v0.0.0-20220111093109-d55c255bac03 h1:0FB83qp0AzVJm+0wcIlauAjJ+tNdh7jLuacRYCIVv7s= 170 | golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 171 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= 172 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 173 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 174 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 175 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 176 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 177 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 179 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 180 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 182 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 183 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 195 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 196 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 197 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 198 | golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= 199 | golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 200 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= 201 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 202 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 203 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 204 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 205 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 206 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 207 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 208 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 209 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 210 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= 211 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 212 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 213 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 214 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 215 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 216 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 217 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 218 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 219 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 220 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 221 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 222 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 223 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 224 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 225 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 226 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 227 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 228 | google.golang.org/genproto v0.0.0-20220112215332-a9c7c0acf9f2 h1:z+R4M/SuyaRsj1zu3WC+nIQyfSrSIpuDcY01/R3uCtg= 229 | google.golang.org/genproto v0.0.0-20220112215332-a9c7c0acf9f2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 230 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 231 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 232 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 233 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 234 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 235 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 236 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 237 | google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= 238 | google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= 239 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 240 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 241 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 242 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 243 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 244 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 245 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 246 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 247 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 248 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 249 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 250 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 251 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 252 | gopkg.in/DataDog/dd-trace-go.v1 v1.36.0 h1:t2KEcCXajtchpvoIGm0xU+Ytj8KkRyxsXVhWOGg6lEk= 253 | gopkg.in/DataDog/dd-trace-go.v1 v1.36.0/go.mod h1:Cv0Bzs/zTzzrUDSw8Q+q/vC+uwPD+R530npGo0lfiCE= 254 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 255 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 256 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 257 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 258 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 259 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 260 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 261 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 262 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 263 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 264 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 265 | inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c h1:nr31qYr+91rWD8klUkPx3eGTZzumCC414UJG1QRKZTc= 266 | inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c/go.mod h1:KOJdAzQzMLKzwFEdOOnrnSrLIhaFVB+NQoME/e5wllA= 267 | -------------------------------------------------------------------------------- /gwlb/active_flow.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "fmt" 8 | "github.com/aidansteele/flowdog/gwlb/mirror" 9 | "github.com/aidansteele/flowdog/mytls" 10 | "github.com/google/gopacket" 11 | "github.com/google/gopacket/layers" 12 | "github.com/pkg/errors" 13 | "golang.org/x/sync/errgroup" 14 | "inet.af/netstack/tcpip" 15 | "inet.af/netstack/tcpip/adapters/gonet" 16 | "inet.af/netstack/tcpip/buffer" 17 | "inet.af/netstack/tcpip/header" 18 | "inet.af/netstack/tcpip/link/channel" 19 | "inet.af/netstack/tcpip/network/ipv4" 20 | "inet.af/netstack/tcpip/stack" 21 | "inet.af/netstack/tcpip/transport/tcp" 22 | "io" 23 | "net" 24 | "net/http" 25 | "time" 26 | ) 27 | 28 | const ( 29 | timeoutTcp = 350 * time.Second 30 | timeoutNonTcp = 120 * time.Second 31 | ) 32 | 33 | var errTimeout = errors.New("gwlb timeout") 34 | 35 | type activeFlow struct { 36 | geneveHeader []byte 37 | gwlbConn *net.UDPConn 38 | mirror chan mirror.Packet 39 | endpoint *channel.Endpoint 40 | stack *stack.Stack 41 | httpReady chan struct{} 42 | } 43 | 44 | type FlowAcceptor interface { 45 | AcceptFlow(ctx context.Context, pkt gopacket.Packet, opts AwsGeneveOptions) bool 46 | } 47 | 48 | type newFlowOptions struct { 49 | acceptor FlowAcceptor 50 | handler http.Handler 51 | keyLogger io.Writer 52 | mirror chan mirror.Packet 53 | } 54 | 55 | func newFlow(ctx context.Context, ch chan genevePacket, geneveOpts AwsGeneveOptions, options newFlowOptions) { 56 | ctx = ContextWithGeneveOptions(ctx, geneveOpts) 57 | 58 | // retrieve first packet in flow to inspect ip, port, etc 59 | pkt := <-ch 60 | // then we reinject for forwarding/interception/whatever 61 | go func() { ch <- pkt }() 62 | 63 | if options.acceptor != nil && !options.acceptor.AcceptFlow(ctx, pkt.pkt, geneveOpts) { 64 | return // TODO: this should probably send a refusal (for tcp) instead of silent dropping 65 | } 66 | 67 | fmt.Printf("%s new flow vpcEndpointId=%016x attachmentId=%016x flowCookie=%08x\n", time.Now().String(), geneveOpts.VpcEndpointId, geneveOpts.AttachmentId, geneveOpts.FlowCookie) 68 | gwlbConn := getUdpConn(pkt.addr) 69 | 70 | ipLayer, isIpv4 := pkt.pkt.Layer(layers.LayerTypeIPv4).(*layers.IPv4) 71 | tcpLayer, isTcp := pkt.pkt.TransportLayer().(*layers.TCP) 72 | 73 | if !isTcp { 74 | fmt.Printf("fast-path for non-tcp flow=%08x\n", geneveOpts.FlowCookie) 75 | fastPath(ctx, ch, gwlbConn, options.mirror, timeoutNonTcp) 76 | return 77 | } 78 | 79 | if !isIpv4 { 80 | fmt.Printf("fast-path for non-ipv4 flow=%08x\n", geneveOpts.FlowCookie) 81 | fastPath(ctx, ch, gwlbConn, options.mirror, timeoutTcp) 82 | return 83 | } 84 | 85 | if options.handler == nil || (tcpLayer.DstPort != 80 && tcpLayer.DstPort != 443) { 86 | fmt.Printf("fast-path for non-port 80/443 flow=%08x\n", geneveOpts.FlowCookie) 87 | fastPath(ctx, ch, gwlbConn, options.mirror, timeoutTcp) 88 | return 89 | } 90 | 91 | geneveLayer := pkt.pkt.Layer(layers.LayerTypeGeneve).(*layers.Geneve) 92 | contents := geneveLayer.LayerContents() 93 | hdr := make([]byte, len(contents)) 94 | copy(hdr, contents) 95 | 96 | sourceAddr := &net.TCPAddr{IP: ipLayer.SrcIP, Port: int(tcpLayer.SrcPort)} 97 | destAddr := &net.TCPAddr{IP: ipLayer.DstIP, Port: int(tcpLayer.DstPort)} 98 | ctx = ContextWithAddrs(ctx, sourceAddr, destAddr) 99 | 100 | endpoint, netstack := newEndpointAndStack(geneveOpts) 101 | ctx = ContextWithNetstack(ctx, netstack) 102 | 103 | a := &activeFlow{ 104 | geneveHeader: hdr, 105 | gwlbConn: gwlbConn, 106 | endpoint: endpoint, 107 | mirror: options.mirror, 108 | stack: netstack, 109 | httpReady: make(chan struct{}, 1), 110 | } 111 | 112 | g, ctx := errgroup.WithContext(ctx) 113 | 114 | g.Go(func() error { 115 | return a.runNetstackToGwlb(ctx) 116 | }) 117 | 118 | g.Go(func() error { 119 | config := mytls.TlsConfig().Clone() 120 | config.KeyLogWriter = options.keyLogger 121 | return a.runProxyHttp(ctx, options.handler, config) 122 | }) 123 | 124 | g.Go(func() error { 125 | <-a.httpReady 126 | return a.runGwlbToNetstack(ctx, ch, timeoutTcp) 127 | }) 128 | 129 | err := g.Wait() 130 | if err == errTimeout { 131 | return 132 | } 133 | 134 | if err != nil { 135 | fmt.Printf("%+v\n", err) 136 | panic(err) 137 | } 138 | } 139 | 140 | func (a *activeFlow) runGwlbToNetstack(ctx context.Context, ch chan genevePacket, timeout time.Duration) error { 141 | originatorAddr, _ := AddrsFromContext(ctx) 142 | originator := originatorAddr.IP.String() 143 | 144 | for { 145 | select { 146 | case <-ctx.Done(): 147 | return ctx.Err() 148 | case <-time.After(timeout): 149 | return errTimeout 150 | case pkt := <-ch: 151 | geneveLayer := pkt.pkt.Layer(layers.LayerTypeGeneve).(*layers.Geneve) 152 | payload := geneveLayer.LayerPayload() 153 | 154 | // TODO ipv6 155 | ipLayer := pkt.pkt.NetworkLayer().(*layers.IPv4) 156 | 157 | // TODO: avoid stringification in hot path 158 | a.mirror <- mirror.New(pkt.buf, ipLayer.SrcIP.String() == originator) 159 | 160 | data := buffer.NewVectorisedView(1, []buffer.View{payload}) 161 | newPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{Data: data}) 162 | a.endpoint.InjectInbound(ipv4.ProtocolNumber, newPkt) 163 | newPkt.DecRef() 164 | } 165 | } 166 | } 167 | 168 | func (a *activeFlow) runNetstackToGwlb(ctx context.Context) error { 169 | buf := &bytes.Buffer{} 170 | originatorAddr, _ := AddrsFromContext(ctx) 171 | originator := originatorAddr.IP.String() 172 | 173 | for { 174 | pinfo, more := a.endpoint.ReadContext(ctx) 175 | if !more { 176 | fmt.Printf("exiting flow with cookie %08x\n", GeneveOptionsFromContext(ctx).FlowCookie) 177 | return ctx.Err() 178 | } 179 | 180 | data := buffer.NewVectorisedView(pinfo.Pkt.Size(), pinfo.Pkt.Views()) 181 | view := data.ToView() 182 | 183 | buf.Write(a.geneveHeader) 184 | buf.Write(view) 185 | _, err := a.gwlbConn.Write(buf.Bytes()) 186 | if err != nil { 187 | return errors.WithStack(err) 188 | } 189 | 190 | // TODO: avoid stringification in hot path 191 | a.mirror <- mirror.New(buf.Bytes(), pinfo.Route.RemoteAddress.String() == originator) 192 | buf.Reset() 193 | } 194 | } 195 | 196 | func (a *activeFlow) runProxyHttp(ctx context.Context, handler http.Handler, config *tls.Config) error { 197 | g, ctx := errgroup.WithContext(ctx) 198 | 199 | httpSrv := &http.Server{ 200 | Handler: handler, 201 | TLSConfig: config, 202 | BaseContext: func(listener net.Listener) context.Context { return ctx }, 203 | } 204 | 205 | httpSrv.TLSConfig.NextProtos = []string{"h2", "http/1.1"} 206 | 207 | port443, err := gonet.ListenTCP(a.stack, tcpip.FullAddress{Port: 443}, ipv4.ProtocolNumber) 208 | if err != nil { 209 | return errors.WithStack(err) 210 | } 211 | 212 | port80, err := gonet.ListenTCP(a.stack, tcpip.FullAddress{Port: 80}, ipv4.ProtocolNumber) 213 | if err != nil { 214 | return errors.WithStack(err) 215 | } 216 | 217 | a.httpReady <- struct{}{} 218 | 219 | g.Go(func() error { 220 | err := httpSrv.Serve(port80) 221 | return errors.WithStack(err) 222 | }) 223 | 224 | g.Go(func() error { 225 | err := httpSrv.ServeTLS(port443, "", "") 226 | return errors.WithStack(err) 227 | }) 228 | 229 | g.Go(func() error { 230 | <-ctx.Done() 231 | return httpSrv.Close() 232 | }) 233 | 234 | return g.Wait() 235 | } 236 | 237 | func newEndpointAndStack(opts AwsGeneveOptions) (*channel.Endpoint, *stack.Stack) { 238 | linkAddr, _ := tcpip.ParseMACAddress("aa:bb:cc:dd:ee:ff") 239 | endpoint := channel.New(200, 8500, linkAddr) 240 | 241 | s := stack.New(stack.Options{ 242 | NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, 243 | TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol}, 244 | }) 245 | 246 | // Add default route. 247 | s.SetRouteTable([]tcpip.Route{ 248 | {NIC: NICID, Destination: header.IPv4EmptySubnet}, 249 | }) 250 | 251 | linkEndpoint := endpoint 252 | //linkEndpoint := sniffer.NewWithPrefix(endpoint, fmt.Sprintf("flow-%08x ", opts.flowCookie)) 253 | tcpErr := s.CreateNIC(NICID, linkEndpoint) 254 | if tcpErr != nil { 255 | fmt.Printf("%+v\n", tcpErr) 256 | panic(tcpErr) 257 | } 258 | 259 | s.SetPromiscuousMode(NICID, true) 260 | s.SetSpoofing(NICID, true) 261 | return endpoint, s 262 | } 263 | 264 | const NICID = 1 265 | -------------------------------------------------------------------------------- /gwlb/context.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "context" 5 | "inet.af/netstack/tcpip/stack" 6 | "net" 7 | ) 8 | 9 | type contextKey string 10 | 11 | const ( 12 | contextKeyGeneveOptions = contextKey("contextKeyGeneveOptions") 13 | contextKeyAddrs = contextKey("contextKeyAddrs") 14 | contextKeyNetstack = contextKey("contextKeyNetstack") 15 | ) 16 | 17 | func ContextWithGeneveOptions(ctx context.Context, opts AwsGeneveOptions) context.Context { 18 | return context.WithValue(ctx, contextKeyGeneveOptions, opts) 19 | } 20 | 21 | func GeneveOptionsFromContext(ctx context.Context) AwsGeneveOptions { 22 | opts, _ := ctx.Value(contextKeyGeneveOptions).(AwsGeneveOptions) 23 | return opts 24 | } 25 | 26 | func ContextWithAddrs(ctx context.Context, source, dest *net.TCPAddr) context.Context { 27 | slice := []*net.TCPAddr{source, dest} 28 | return context.WithValue(ctx, contextKeyAddrs, slice) 29 | } 30 | 31 | func AddrsFromContext(ctx context.Context) (source *net.TCPAddr, dest *net.TCPAddr) { 32 | slice := ctx.Value(contextKeyAddrs).([]*net.TCPAddr) 33 | return slice[0], slice[1] 34 | } 35 | 36 | func ContextWithNetstack(ctx context.Context, netstack *stack.Stack) context.Context { 37 | return context.WithValue(ctx, contextKeyNetstack, netstack) 38 | } 39 | 40 | func NetstackFromContext(ctx context.Context) *stack.Stack { 41 | return ctx.Value(contextKeyNetstack).(*stack.Stack) 42 | } 43 | -------------------------------------------------------------------------------- /gwlb/default_handler.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | "net/http/httputil" 8 | "time" 9 | ) 10 | 11 | func DefaultHandler(interceptor Interceptor, tlsClientConfig *tls.Config) http.Handler { 12 | rp := &httputil.ReverseProxy{ 13 | ModifyResponse: interceptor.OnResponse, 14 | Director: interceptor.OnRequest, 15 | Transport: &http.Transport{ 16 | DialContext: DialContext, 17 | // these are copied from stdlib default 18 | ForceAttemptHTTP2: true, 19 | MaxIdleConns: 100, 20 | IdleConnTimeout: 90 * time.Second, 21 | TLSHandshakeTimeout: 10 * time.Second, 22 | ExpectContinueTimeout: 1 * time.Second, 23 | TLSClientConfig: tlsClientConfig, 24 | }, 25 | } 26 | 27 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | ctx := r.Context() 29 | 30 | r.URL.Host = r.Host 31 | port := ctx.Value(http.LocalAddrContextKey).(*net.TCPAddr).Port 32 | if port == 443 { 33 | r.URL.Scheme = "https" 34 | } else if port == 80 { 35 | r.URL.Scheme = "http" 36 | } 37 | 38 | if r.Header.Get("Upgrade") == "websocket" { 39 | proxyWebsocket(w, r, tlsClientConfig) 40 | } else { 41 | rp.ServeHTTP(w, r) 42 | } 43 | }) 44 | 45 | return h 46 | } 47 | -------------------------------------------------------------------------------- /gwlb/dial.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "context" 5 | "inet.af/netstack/tcpip" 6 | "inet.af/netstack/tcpip/adapters/gonet" 7 | "inet.af/netstack/tcpip/network/ipv4" 8 | "net" 9 | "net/http" 10 | ) 11 | 12 | func DialContext(ctx context.Context, network, addr string) (net.Conn, error) { 13 | intendedAddr := ctx.Value(http.LocalAddrContextKey).(*net.TCPAddr) 14 | sourceAddr, _ := AddrsFromContext(ctx) 15 | netstack := NetstackFromContext(ctx) 16 | 17 | remote := tcpip.FullAddress{Addr: tcpip.Address(intendedAddr.IP.To4()), Port: uint16(intendedAddr.Port)} 18 | local := tcpip.FullAddress{Addr: tcpip.Address(sourceAddr.IP), Port: uint16(sourceAddr.Port)} 19 | return gonet.DialTCPWithBind(ctx, netstack, local, remote, ipv4.ProtocolNumber) 20 | } 21 | -------------------------------------------------------------------------------- /gwlb/fastpath.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aidansteele/flowdog/gwlb/mirror" 7 | "net" 8 | "time" 9 | ) 10 | 11 | func fastPath(ctx context.Context, ch chan genevePacket, gwlbConn *net.UDPConn, mirrorch chan mirror.Packet, timeout time.Duration) { 12 | for { 13 | select { 14 | case <-ctx.Done(): 15 | return 16 | case pkt := <-ch: 17 | _, err := gwlbConn.Write(pkt.buf) 18 | if err != nil { 19 | fmt.Printf("%+v\n", err) 20 | panic(err) 21 | } 22 | mirrorch <- mirror.New(pkt.buf, true) 23 | case <-time.After(timeout): 24 | return 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gwlb/geneve_options.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/google/gopacket" 6 | "github.com/google/gopacket/layers" 7 | ) 8 | 9 | const ( 10 | AwsGeneveClass = 0x108 11 | AwsGeneveTypeVpcEndpointId = 0x1 12 | AwsGeneveTypeAttachmentId = 0x2 13 | AwsGeneveTypeFlowCookie = 0x3 14 | ) 15 | 16 | type AwsGeneveOptions struct { 17 | VpcEndpointId uint64 18 | AttachmentId uint64 19 | FlowCookie uint32 20 | } 21 | 22 | func ExtractAwsGeneveOptions(pkt gopacket.Packet) AwsGeneveOptions { 23 | geneveLayer := pkt.Layer(layers.LayerTypeGeneve).(*layers.Geneve) 24 | 25 | opts := AwsGeneveOptions{} 26 | 27 | for _, option := range geneveLayer.Options { 28 | if option.Class != AwsGeneveClass { 29 | continue 30 | } 31 | 32 | switch option.Type { 33 | case AwsGeneveTypeVpcEndpointId: 34 | opts.VpcEndpointId = binary.BigEndian.Uint64(option.Data) 35 | case AwsGeneveTypeAttachmentId: 36 | opts.AttachmentId = binary.BigEndian.Uint64(option.Data) 37 | case AwsGeneveTypeFlowCookie: 38 | opts.FlowCookie = binary.BigEndian.Uint32(option.Data) 39 | default: 40 | // no-op 41 | } 42 | } 43 | 44 | return opts 45 | } 46 | -------------------------------------------------------------------------------- /gwlb/interceptors.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import "net/http" 4 | 5 | // Interceptor is modelled on https://pkg.go.dev/net/http/httputil#ReverseProxy 6 | type Interceptor interface { 7 | OnRequest(req *http.Request) 8 | OnResponse(resp *http.Response) error 9 | } 10 | 11 | type Chain []Interceptor 12 | 13 | func (c Chain) OnRequest(req *http.Request) { 14 | for _, interceptor := range c { 15 | interceptor.OnRequest(req) 16 | } 17 | } 18 | 19 | func (c Chain) OnResponse(resp *http.Response) error { 20 | for _, interceptor := range c { 21 | err := interceptor.OnResponse(resp) 22 | if err != nil { 23 | return err 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /gwlb/mirror/mirrored_packet.go: -------------------------------------------------------------------------------- 1 | package mirror 2 | 3 | type Type int 4 | 5 | const ( 6 | TypeUnknown = Type(iota) 7 | TypePreRewrite 8 | TypePostRewrite 9 | ) 10 | 11 | type Packet struct { 12 | Packet []byte 13 | Type Type 14 | } 15 | 16 | func New(pkt []byte, pre bool) Packet { 17 | mirrored := make([]byte, len(pkt)) 18 | copy(mirrored, pkt) 19 | 20 | if pre { 21 | return Packet{Packet: mirrored, Type: TypePreRewrite} 22 | } else { 23 | return Packet{Packet: mirrored, Type: TypePostRewrite} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gwlb/serve.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aidansteele/flowdog/gwlb/mirror" 7 | "github.com/google/gopacket" 8 | "github.com/google/gopacket/layers" 9 | "github.com/pkg/errors" 10 | "io" 11 | "net" 12 | "net/http" 13 | ) 14 | 15 | type genevePacket struct { 16 | buf []byte 17 | addr *net.UDPAddr 18 | pkt gopacket.Packet 19 | } 20 | 21 | type Server struct { 22 | Acceptor FlowAcceptor 23 | Handler http.Handler 24 | KeyLogWriter io.Writer 25 | Mirror chan mirror.Packet 26 | } 27 | 28 | func (s *Server) Serve(ctx context.Context, conn *net.UDPConn) error { 29 | flows := map[uint32]chan genevePacket{} 30 | flowEndedCh := make(chan uint32) 31 | 32 | pktbuf := make([]byte, 16_384) 33 | 34 | for { 35 | select { 36 | case <-ctx.Done(): 37 | return ctx.Err() 38 | case flowCookie := <-flowEndedCh: 39 | fmt.Printf("deleting flow %08x\n", flowCookie) 40 | delete(flows, flowCookie) 41 | default: 42 | n, addr, err := conn.ReadFromUDP(pktbuf) 43 | if err != nil { 44 | return errors.WithStack(err) 45 | } 46 | 47 | newbuf := make([]byte, n) 48 | copy(newbuf, pktbuf[:n]) 49 | 50 | pkt := gopacket.NewPacket(newbuf, layers.LayerTypeGeneve, gopacket.Default) 51 | opts := ExtractAwsGeneveOptions(pkt) 52 | 53 | cookie := opts.FlowCookie 54 | ch, found := flows[cookie] 55 | if !found { 56 | ch = make(chan genevePacket) 57 | flows[cookie] = ch 58 | go func() { 59 | newFlow(ctx, ch, opts, newFlowOptions{ 60 | acceptor: s.Acceptor, 61 | handler: s.Handler, 62 | mirror: s.Mirror, 63 | keyLogger: s.KeyLogWriter, 64 | }) 65 | flowEndedCh <- cookie 66 | }() 67 | } 68 | 69 | ch <- genevePacket{ 70 | buf: newbuf, 71 | addr: addr, 72 | pkt: pkt, 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /gwlb/shark/filter.go: -------------------------------------------------------------------------------- 1 | package shark 2 | 3 | import ( 4 | "github.com/google/gopacket/layers" 5 | "github.com/google/gopacket/pcap" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func FilterVM(input string) (*pcap.BPF, error) { 10 | vm, err := pcap.NewBPF(layers.LinkType(12), 0x10000, input) 11 | return vm, errors.WithStack(err) 12 | } 13 | -------------------------------------------------------------------------------- /gwlb/shark/shark.go: -------------------------------------------------------------------------------- 1 | package shark 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "github.com/aidansteele/flowdog/gwlb/mirror" 9 | "github.com/google/gopacket" 10 | "github.com/google/gopacket/layers" 11 | "github.com/google/gopacket/pcap" 12 | "github.com/pkg/errors" 13 | "google.golang.org/grpc" 14 | "google.golang.org/protobuf/types/known/timestamppb" 15 | "io" 16 | "net" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | //go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative shark.proto 22 | 23 | type SharkServer struct { 24 | keyLogWriter io.Writer 25 | keyLogReader io.Reader 26 | ctx context.Context 27 | clients []*client 28 | mut sync.Mutex 29 | } 30 | 31 | func NewSharkServer() *SharkServer { 32 | pr, pw := io.Pipe() 33 | 34 | return &SharkServer{keyLogWriter: pw, keyLogReader: pr} 35 | } 36 | 37 | func (s *SharkServer) Serve(ctx context.Context, l net.Listener, ch chan mirror.Packet) error { 38 | s.ctx = ctx 39 | go s.run(ch) 40 | 41 | go s.streamSslKeyLog(ctx) 42 | 43 | gs := grpc.NewServer() 44 | RegisterVpcsharkServer(gs, s) 45 | 46 | go func() { 47 | <-ctx.Done() 48 | gs.Stop() 49 | }() 50 | 51 | err := gs.Serve(l) 52 | return errors.WithStack(err) 53 | } 54 | 55 | func (s *SharkServer) KeyLogWriter() io.Writer { 56 | return s.keyLogWriter 57 | } 58 | 59 | func (s *SharkServer) streamSslKeyLog(ctx context.Context) { 60 | scan := bufio.NewScanner(s.keyLogReader) 61 | buf := &bytes.Buffer{} 62 | 63 | for scan.Scan() { 64 | if ctx.Err() != nil { 65 | return 66 | } 67 | 68 | buf.Write(scan.Bytes()) 69 | buf.WriteByte('\n') 70 | s.forEachClient(func(c *client) error { 71 | return c.stream.Send(&GetPacketsOutput{ 72 | Time: timestamppb.New(time.Now()), 73 | SslKeyLog: buf.Bytes(), 74 | }) 75 | }) 76 | buf.Reset() 77 | } 78 | } 79 | 80 | func (s *SharkServer) forEachClient(fn func(c *client) error) { 81 | s.mut.Lock() 82 | defer s.mut.Unlock() 83 | 84 | validLen := 0 85 | 86 | for _, stream := range s.clients { 87 | err := fn(stream) 88 | if err == nil { 89 | s.clients[validLen] = stream 90 | validLen++ 91 | } 92 | } 93 | 94 | // nil out invalid clients to garbage collect them 95 | for idx := validLen; idx < len(s.clients); idx++ { 96 | fmt.Println("removing an invalid shark listener") 97 | s.clients[idx] = nil 98 | } 99 | s.clients = s.clients[:validLen] 100 | } 101 | 102 | func (s *SharkServer) run(ch chan mirror.Packet) { 103 | for { 104 | select { 105 | case <-s.ctx.Done(): 106 | return 107 | case pkt := <-ch: 108 | geneve := gopacket.NewPacket(pkt.Packet, layers.LayerTypeGeneve, gopacket.Default).Layer(layers.LayerTypeGeneve).(*layers.Geneve) 109 | s.forEachClient(func(c *client) error { 110 | if c.mirrorType != pkt.Type { 111 | return nil 112 | } 113 | 114 | match := c.vm.Matches(gopacket.CaptureInfo{}, geneve.LayerPayload()) 115 | if !match { 116 | return nil 117 | } 118 | 119 | return c.stream.Send(&GetPacketsOutput{ 120 | Time: timestamppb.New(time.Now()), 121 | Payload: pkt.Packet, 122 | }) 123 | }) 124 | } 125 | } 126 | } 127 | 128 | type client struct { 129 | stream Vpcshark_GetPacketsServer 130 | vm *pcap.BPF 131 | mirrorType mirror.Type 132 | } 133 | 134 | func (s *SharkServer) GetPackets(input *GetPacketsInput, stream Vpcshark_GetPacketsServer) error { 135 | vm, err := FilterVM(input.Filter) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | mirrorType := mirror.TypeUnknown 141 | switch input.PacketType { 142 | case PacketType_PRE: 143 | mirrorType = mirror.TypePreRewrite 144 | case PacketType_POST: 145 | mirrorType = mirror.TypePostRewrite 146 | case PacketType_UNKNOWN: 147 | fallthrough 148 | default: 149 | return errors.New("unknown packet type") 150 | } 151 | 152 | s.mut.Lock() 153 | s.clients = append(s.clients, &client{stream: stream, vm: vm, mirrorType: mirrorType}) 154 | s.mut.Unlock() 155 | 156 | <-s.ctx.Done() 157 | return nil 158 | } 159 | 160 | func (s *SharkServer) mustEmbedUnimplementedVpcsharkServer() {} 161 | -------------------------------------------------------------------------------- /gwlb/shark/shark.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.19.3 5 | // source: shark.proto 6 | 7 | package shark 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | timestamppb "google.golang.org/protobuf/types/known/timestamppb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type PacketType int32 25 | 26 | const ( 27 | PacketType_UNKNOWN PacketType = 0 28 | PacketType_PRE PacketType = 1 29 | PacketType_POST PacketType = 2 30 | ) 31 | 32 | // Enum value maps for PacketType. 33 | var ( 34 | PacketType_name = map[int32]string{ 35 | 0: "UNKNOWN", 36 | 1: "PRE", 37 | 2: "POST", 38 | } 39 | PacketType_value = map[string]int32{ 40 | "UNKNOWN": 0, 41 | "PRE": 1, 42 | "POST": 2, 43 | } 44 | ) 45 | 46 | func (x PacketType) Enum() *PacketType { 47 | p := new(PacketType) 48 | *p = x 49 | return p 50 | } 51 | 52 | func (x PacketType) String() string { 53 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 54 | } 55 | 56 | func (PacketType) Descriptor() protoreflect.EnumDescriptor { 57 | return file_shark_proto_enumTypes[0].Descriptor() 58 | } 59 | 60 | func (PacketType) Type() protoreflect.EnumType { 61 | return &file_shark_proto_enumTypes[0] 62 | } 63 | 64 | func (x PacketType) Number() protoreflect.EnumNumber { 65 | return protoreflect.EnumNumber(x) 66 | } 67 | 68 | // Deprecated: Use PacketType.Descriptor instead. 69 | func (PacketType) EnumDescriptor() ([]byte, []int) { 70 | return file_shark_proto_rawDescGZIP(), []int{0} 71 | } 72 | 73 | type GetPacketsInput struct { 74 | state protoimpl.MessageState 75 | sizeCache protoimpl.SizeCache 76 | unknownFields protoimpl.UnknownFields 77 | 78 | Filter string `protobuf:"bytes,1,opt,name=Filter,proto3" json:"Filter,omitempty"` 79 | PacketType PacketType `protobuf:"varint,2,opt,name=PacketType,proto3,enum=PacketType" json:"PacketType,omitempty"` 80 | } 81 | 82 | func (x *GetPacketsInput) Reset() { 83 | *x = GetPacketsInput{} 84 | if protoimpl.UnsafeEnabled { 85 | mi := &file_shark_proto_msgTypes[0] 86 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 87 | ms.StoreMessageInfo(mi) 88 | } 89 | } 90 | 91 | func (x *GetPacketsInput) String() string { 92 | return protoimpl.X.MessageStringOf(x) 93 | } 94 | 95 | func (*GetPacketsInput) ProtoMessage() {} 96 | 97 | func (x *GetPacketsInput) ProtoReflect() protoreflect.Message { 98 | mi := &file_shark_proto_msgTypes[0] 99 | if protoimpl.UnsafeEnabled && x != nil { 100 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 101 | if ms.LoadMessageInfo() == nil { 102 | ms.StoreMessageInfo(mi) 103 | } 104 | return ms 105 | } 106 | return mi.MessageOf(x) 107 | } 108 | 109 | // Deprecated: Use GetPacketsInput.ProtoReflect.Descriptor instead. 110 | func (*GetPacketsInput) Descriptor() ([]byte, []int) { 111 | return file_shark_proto_rawDescGZIP(), []int{0} 112 | } 113 | 114 | func (x *GetPacketsInput) GetFilter() string { 115 | if x != nil { 116 | return x.Filter 117 | } 118 | return "" 119 | } 120 | 121 | func (x *GetPacketsInput) GetPacketType() PacketType { 122 | if x != nil { 123 | return x.PacketType 124 | } 125 | return PacketType_UNKNOWN 126 | } 127 | 128 | type GetPacketsOutput struct { 129 | state protoimpl.MessageState 130 | sizeCache protoimpl.SizeCache 131 | unknownFields protoimpl.UnknownFields 132 | 133 | Payload []byte `protobuf:"bytes,1,opt,name=Payload,proto3" json:"Payload,omitempty"` 134 | Time *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=Time,proto3" json:"Time,omitempty"` 135 | SslKeyLog []byte `protobuf:"bytes,3,opt,name=SslKeyLog,proto3" json:"SslKeyLog,omitempty"` 136 | } 137 | 138 | func (x *GetPacketsOutput) Reset() { 139 | *x = GetPacketsOutput{} 140 | if protoimpl.UnsafeEnabled { 141 | mi := &file_shark_proto_msgTypes[1] 142 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 143 | ms.StoreMessageInfo(mi) 144 | } 145 | } 146 | 147 | func (x *GetPacketsOutput) String() string { 148 | return protoimpl.X.MessageStringOf(x) 149 | } 150 | 151 | func (*GetPacketsOutput) ProtoMessage() {} 152 | 153 | func (x *GetPacketsOutput) ProtoReflect() protoreflect.Message { 154 | mi := &file_shark_proto_msgTypes[1] 155 | if protoimpl.UnsafeEnabled && x != nil { 156 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 157 | if ms.LoadMessageInfo() == nil { 158 | ms.StoreMessageInfo(mi) 159 | } 160 | return ms 161 | } 162 | return mi.MessageOf(x) 163 | } 164 | 165 | // Deprecated: Use GetPacketsOutput.ProtoReflect.Descriptor instead. 166 | func (*GetPacketsOutput) Descriptor() ([]byte, []int) { 167 | return file_shark_proto_rawDescGZIP(), []int{1} 168 | } 169 | 170 | func (x *GetPacketsOutput) GetPayload() []byte { 171 | if x != nil { 172 | return x.Payload 173 | } 174 | return nil 175 | } 176 | 177 | func (x *GetPacketsOutput) GetTime() *timestamppb.Timestamp { 178 | if x != nil { 179 | return x.Time 180 | } 181 | return nil 182 | } 183 | 184 | func (x *GetPacketsOutput) GetSslKeyLog() []byte { 185 | if x != nil { 186 | return x.SslKeyLog 187 | } 188 | return nil 189 | } 190 | 191 | var File_shark_proto protoreflect.FileDescriptor 192 | 193 | var file_shark_proto_rawDesc = []byte{ 194 | 0x0a, 0x0b, 0x73, 0x68, 0x61, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 195 | 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 196 | 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x56, 197 | 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x49, 0x6e, 0x70, 0x75, 198 | 0x74, 0x12, 0x16, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 199 | 0x09, 0x52, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0a, 0x50, 0x61, 0x63, 200 | 0x6b, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 201 | 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x50, 0x61, 0x63, 0x6b, 202 | 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x22, 0x7a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x50, 0x61, 0x63, 203 | 0x6b, 0x65, 0x74, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x61, 204 | 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x50, 0x61, 0x79, 205 | 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 206 | 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 207 | 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 208 | 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x73, 0x6c, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 209 | 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53, 0x73, 0x6c, 0x4b, 0x65, 0x79, 0x4c, 210 | 0x6f, 0x67, 0x2a, 0x2c, 0x0a, 0x0a, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 211 | 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 212 | 0x03, 0x50, 0x52, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x53, 0x54, 0x10, 0x02, 213 | 0x32, 0x41, 0x0a, 0x08, 0x56, 0x70, 0x63, 0x73, 0x68, 0x61, 0x72, 0x6b, 0x12, 0x35, 0x0a, 0x0a, 214 | 0x47, 0x65, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x10, 0x2e, 0x47, 0x65, 0x74, 215 | 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x1a, 0x11, 0x2e, 0x47, 216 | 0x65, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 217 | 0x00, 0x30, 0x01, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 218 | 0x6d, 0x2f, 0x61, 0x69, 0x64, 0x61, 0x6e, 0x73, 0x74, 0x65, 0x65, 0x6c, 0x65, 0x2f, 0x66, 0x6c, 219 | 0x6f, 0x77, 0x64, 0x6f, 0x67, 0x2f, 0x67, 0x77, 0x6c, 0x62, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x6b, 220 | 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 221 | } 222 | 223 | var ( 224 | file_shark_proto_rawDescOnce sync.Once 225 | file_shark_proto_rawDescData = file_shark_proto_rawDesc 226 | ) 227 | 228 | func file_shark_proto_rawDescGZIP() []byte { 229 | file_shark_proto_rawDescOnce.Do(func() { 230 | file_shark_proto_rawDescData = protoimpl.X.CompressGZIP(file_shark_proto_rawDescData) 231 | }) 232 | return file_shark_proto_rawDescData 233 | } 234 | 235 | var file_shark_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 236 | var file_shark_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 237 | var file_shark_proto_goTypes = []interface{}{ 238 | (PacketType)(0), // 0: PacketType 239 | (*GetPacketsInput)(nil), // 1: GetPacketsInput 240 | (*GetPacketsOutput)(nil), // 2: GetPacketsOutput 241 | (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp 242 | } 243 | var file_shark_proto_depIdxs = []int32{ 244 | 0, // 0: GetPacketsInput.PacketType:type_name -> PacketType 245 | 3, // 1: GetPacketsOutput.Time:type_name -> google.protobuf.Timestamp 246 | 1, // 2: Vpcshark.GetPackets:input_type -> GetPacketsInput 247 | 2, // 3: Vpcshark.GetPackets:output_type -> GetPacketsOutput 248 | 3, // [3:4] is the sub-list for method output_type 249 | 2, // [2:3] is the sub-list for method input_type 250 | 2, // [2:2] is the sub-list for extension type_name 251 | 2, // [2:2] is the sub-list for extension extendee 252 | 0, // [0:2] is the sub-list for field type_name 253 | } 254 | 255 | func init() { file_shark_proto_init() } 256 | func file_shark_proto_init() { 257 | if File_shark_proto != nil { 258 | return 259 | } 260 | if !protoimpl.UnsafeEnabled { 261 | file_shark_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 262 | switch v := v.(*GetPacketsInput); i { 263 | case 0: 264 | return &v.state 265 | case 1: 266 | return &v.sizeCache 267 | case 2: 268 | return &v.unknownFields 269 | default: 270 | return nil 271 | } 272 | } 273 | file_shark_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 274 | switch v := v.(*GetPacketsOutput); i { 275 | case 0: 276 | return &v.state 277 | case 1: 278 | return &v.sizeCache 279 | case 2: 280 | return &v.unknownFields 281 | default: 282 | return nil 283 | } 284 | } 285 | } 286 | type x struct{} 287 | out := protoimpl.TypeBuilder{ 288 | File: protoimpl.DescBuilder{ 289 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 290 | RawDescriptor: file_shark_proto_rawDesc, 291 | NumEnums: 1, 292 | NumMessages: 2, 293 | NumExtensions: 0, 294 | NumServices: 1, 295 | }, 296 | GoTypes: file_shark_proto_goTypes, 297 | DependencyIndexes: file_shark_proto_depIdxs, 298 | EnumInfos: file_shark_proto_enumTypes, 299 | MessageInfos: file_shark_proto_msgTypes, 300 | }.Build() 301 | File_shark_proto = out.File 302 | file_shark_proto_rawDesc = nil 303 | file_shark_proto_goTypes = nil 304 | file_shark_proto_depIdxs = nil 305 | } 306 | -------------------------------------------------------------------------------- /gwlb/shark/shark.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/aidansteele/flowdog/gwlb/shark"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | enum PacketType { 8 | UNKNOWN = 0; 9 | PRE = 1; 10 | POST = 2; 11 | } 12 | 13 | message GetPacketsInput { 14 | string Filter = 1; 15 | PacketType PacketType = 2; 16 | } 17 | 18 | message GetPacketsOutput { 19 | bytes Payload = 1; 20 | google.protobuf.Timestamp Time = 2; 21 | bytes SslKeyLog = 3; 22 | } 23 | 24 | service Vpcshark { 25 | rpc GetPackets(GetPacketsInput) returns (stream GetPacketsOutput) {} 26 | } 27 | -------------------------------------------------------------------------------- /gwlb/shark/shark_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v3.19.3 5 | // source: shark.proto 6 | 7 | package shark 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // VpcsharkClient is the client API for Vpcshark service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type VpcsharkClient interface { 25 | GetPackets(ctx context.Context, in *GetPacketsInput, opts ...grpc.CallOption) (Vpcshark_GetPacketsClient, error) 26 | } 27 | 28 | type vpcsharkClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewVpcsharkClient(cc grpc.ClientConnInterface) VpcsharkClient { 33 | return &vpcsharkClient{cc} 34 | } 35 | 36 | func (c *vpcsharkClient) GetPackets(ctx context.Context, in *GetPacketsInput, opts ...grpc.CallOption) (Vpcshark_GetPacketsClient, error) { 37 | stream, err := c.cc.NewStream(ctx, &Vpcshark_ServiceDesc.Streams[0], "/Vpcshark/GetPackets", opts...) 38 | if err != nil { 39 | return nil, err 40 | } 41 | x := &vpcsharkGetPacketsClient{stream} 42 | if err := x.ClientStream.SendMsg(in); err != nil { 43 | return nil, err 44 | } 45 | if err := x.ClientStream.CloseSend(); err != nil { 46 | return nil, err 47 | } 48 | return x, nil 49 | } 50 | 51 | type Vpcshark_GetPacketsClient interface { 52 | Recv() (*GetPacketsOutput, error) 53 | grpc.ClientStream 54 | } 55 | 56 | type vpcsharkGetPacketsClient struct { 57 | grpc.ClientStream 58 | } 59 | 60 | func (x *vpcsharkGetPacketsClient) Recv() (*GetPacketsOutput, error) { 61 | m := new(GetPacketsOutput) 62 | if err := x.ClientStream.RecvMsg(m); err != nil { 63 | return nil, err 64 | } 65 | return m, nil 66 | } 67 | 68 | // VpcsharkServer is the server API for Vpcshark service. 69 | // All implementations must embed UnimplementedVpcsharkServer 70 | // for forward compatibility 71 | type VpcsharkServer interface { 72 | GetPackets(*GetPacketsInput, Vpcshark_GetPacketsServer) error 73 | mustEmbedUnimplementedVpcsharkServer() 74 | } 75 | 76 | // UnimplementedVpcsharkServer must be embedded to have forward compatible implementations. 77 | type UnimplementedVpcsharkServer struct { 78 | } 79 | 80 | func (UnimplementedVpcsharkServer) GetPackets(*GetPacketsInput, Vpcshark_GetPacketsServer) error { 81 | return status.Errorf(codes.Unimplemented, "method GetPackets not implemented") 82 | } 83 | func (UnimplementedVpcsharkServer) mustEmbedUnimplementedVpcsharkServer() {} 84 | 85 | // UnsafeVpcsharkServer may be embedded to opt out of forward compatibility for this service. 86 | // Use of this interface is not recommended, as added methods to VpcsharkServer will 87 | // result in compilation errors. 88 | type UnsafeVpcsharkServer interface { 89 | mustEmbedUnimplementedVpcsharkServer() 90 | } 91 | 92 | func RegisterVpcsharkServer(s grpc.ServiceRegistrar, srv VpcsharkServer) { 93 | s.RegisterService(&Vpcshark_ServiceDesc, srv) 94 | } 95 | 96 | func _Vpcshark_GetPackets_Handler(srv interface{}, stream grpc.ServerStream) error { 97 | m := new(GetPacketsInput) 98 | if err := stream.RecvMsg(m); err != nil { 99 | return err 100 | } 101 | return srv.(VpcsharkServer).GetPackets(m, &vpcsharkGetPacketsServer{stream}) 102 | } 103 | 104 | type Vpcshark_GetPacketsServer interface { 105 | Send(*GetPacketsOutput) error 106 | grpc.ServerStream 107 | } 108 | 109 | type vpcsharkGetPacketsServer struct { 110 | grpc.ServerStream 111 | } 112 | 113 | func (x *vpcsharkGetPacketsServer) Send(m *GetPacketsOutput) error { 114 | return x.ServerStream.SendMsg(m) 115 | } 116 | 117 | // Vpcshark_ServiceDesc is the grpc.ServiceDesc for Vpcshark service. 118 | // It's only intended for direct use with grpc.RegisterService, 119 | // and not to be introspected or modified (even as a copy) 120 | var Vpcshark_ServiceDesc = grpc.ServiceDesc{ 121 | ServiceName: "Vpcshark", 122 | HandlerType: (*VpcsharkServer)(nil), 123 | Methods: []grpc.MethodDesc{}, 124 | Streams: []grpc.StreamDesc{ 125 | { 126 | StreamName: "GetPackets", 127 | Handler: _Vpcshark_GetPackets_Handler, 128 | ServerStreams: true, 129 | }, 130 | }, 131 | Metadata: "shark.proto", 132 | } 133 | -------------------------------------------------------------------------------- /gwlb/udpconn_map.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tidwall/spinlock" 6 | "net" 7 | ) 8 | 9 | var udpLock spinlock.Locker 10 | var udpMap map[int]*net.UDPConn 11 | 12 | func getUdpConn(remote *net.UDPAddr) *net.UDPConn { 13 | udpLock.Lock() 14 | defer udpLock.Unlock() 15 | 16 | lport := remote.Port 17 | if udpMap == nil { 18 | udpMap = map[int]*net.UDPConn{} 19 | } 20 | 21 | gwlbConn := udpMap[lport] 22 | if gwlbConn != nil { 23 | fmt.Println("reusing udp conn") 24 | return gwlbConn 25 | } 26 | 27 | // TODO: gwlb will have a different IP per AZ, right? is hard-coding it here a problem? 28 | raddr := &net.UDPAddr{IP: remote.IP, Port: 6081} 29 | laddr := &net.UDPAddr{Port: lport} 30 | 31 | var err error 32 | gwlbConn, err = net.DialUDP("udp", laddr, raddr) 33 | if err != nil { 34 | fmt.Printf("%+v\n", err) 35 | panic(err) 36 | } 37 | 38 | udpMap[lport] = gwlbConn 39 | return gwlbConn 40 | } 41 | -------------------------------------------------------------------------------- /gwlb/websocket.go: -------------------------------------------------------------------------------- 1 | package gwlb 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "github.com/gorilla/websocket" 8 | "github.com/pkg/errors" 9 | "golang.org/x/sync/errgroup" 10 | "net" 11 | "net/http" 12 | ) 13 | 14 | func proxyWebsocket(w http.ResponseWriter, r *http.Request, tlsClientConfig *tls.Config) { 15 | up := websocket.Upgrader{CheckOrigin: nil} // TODO: check this field 16 | 17 | ctx := r.Context() 18 | port := ctx.Value(http.LocalAddrContextKey).(*net.TCPAddr).Port 19 | if port == 443 { 20 | r.URL.Scheme = "wss" 21 | } else if port == 80 { 22 | r.URL.Scheme = "ws" 23 | } 24 | 25 | newHeaders := r.Header.Clone() 26 | newHeaders.Del("Upgrade") 27 | newHeaders.Del("Connection") 28 | newHeaders.Del("Sec-Websocket-Key") 29 | newHeaders.Del("Sec-Websocket-Version") 30 | newHeaders.Del("Sec-Websocket-Extensions") 31 | newHeaders.Del("Sec-Websocket-Protocol") 32 | 33 | dialer := &websocket.Dialer{ 34 | NetDialContext: DialContext, 35 | TLSClientConfig: tlsClientConfig, 36 | } 37 | 38 | serverConn, resp, err := dialer.DialContext(ctx, r.URL.String(), newHeaders) 39 | if err != nil { 40 | fmt.Printf("wss dial %+v\n", err) 41 | panic(err) 42 | } 43 | 44 | clientConn, err := up.Upgrade(w, r, resp.Header) 45 | if err != nil { 46 | fmt.Printf("%+v\n", err) 47 | panic(err) 48 | } 49 | 50 | g, ctx := errgroup.WithContext(ctx) 51 | 52 | g.Go(func() error { 53 | return wsPumpLoop(ctx, serverConn, clientConn) 54 | }) 55 | 56 | g.Go(func() error { 57 | return wsPumpLoop(ctx, clientConn, serverConn) 58 | }) 59 | 60 | err = g.Wait() 61 | if err != nil { 62 | fmt.Printf("%+v\n", err) 63 | panic(err) 64 | } 65 | } 66 | 67 | func wsPumpLoop(ctx context.Context, a, b *websocket.Conn) error { 68 | for { 69 | select { 70 | case <-ctx.Done(): 71 | return ctx.Err() 72 | default: 73 | mt, p, err := a.ReadMessage() 74 | if err != nil { 75 | return errors.WithStack(err) 76 | } 77 | 78 | err = b.WriteMessage(mt, p) 79 | if err != nil { 80 | return errors.WithStack(err) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /kmssigner/generate/generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/x509" 6 | "crypto/x509/pkix" 7 | "encoding/pem" 8 | "fmt" 9 | "github.com/aidansteele/flowdog/kmssigner" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/kms" 12 | "math/big" 13 | "os" 14 | "time" 15 | ) 16 | 17 | func main() { 18 | keyId := os.Args[1] 19 | 20 | sess, err := session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable, Profile: "ge"}) 21 | if err != nil { 22 | fmt.Printf("%+v\n", err) 23 | panic(err) 24 | } 25 | 26 | tmpl := x509.Certificate{ 27 | Subject: pkix.Name{CommonName: "Aidan KMS TLS Co."}, 28 | SerialNumber: big.NewInt(1), 29 | NotBefore: time.Now(), 30 | NotAfter: time.Now().AddDate(10, 0, 0), 31 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 32 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 33 | BasicConstraintsValid: true, 34 | IsCA: true, 35 | } 36 | 37 | signer, err := kmssigner.New(kms.New(sess), keyId) 38 | if err != nil { 39 | fmt.Printf("%+v\n", err) 40 | panic(err) 41 | } 42 | 43 | der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, signer.Public(), signer) 44 | if err != nil { 45 | fmt.Printf("%+v\n", err) 46 | panic(err) 47 | } 48 | 49 | pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) 50 | fmt.Println(string(pemBytes)) 51 | } 52 | -------------------------------------------------------------------------------- /kmssigner/kmssigner.go: -------------------------------------------------------------------------------- 1 | package kmssigner 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/kms" 8 | "github.com/aws/aws-sdk-go/service/kms/kmsiface" 9 | "github.com/pkg/errors" 10 | "io" 11 | ) 12 | 13 | var signatureMap = map[crypto.Hash]string{ 14 | crypto.SHA256: kms.SigningAlgorithmSpecEcdsaSha256, 15 | crypto.SHA384: kms.SigningAlgorithmSpecEcdsaSha384, 16 | crypto.SHA512: kms.SigningAlgorithmSpecEcdsaSha512, 17 | } 18 | 19 | type signer struct { 20 | api kmsiface.KMSAPI 21 | keyId string 22 | pub crypto.PublicKey 23 | } 24 | 25 | func New(api kmsiface.KMSAPI, keyId string) (crypto.Signer, error) { 26 | getPub, err := api.GetPublicKey(&kms.GetPublicKeyInput{KeyId: &keyId}) 27 | if err != nil { 28 | return nil, errors.WithStack(err) 29 | } 30 | 31 | pub, err := x509.ParsePKIXPublicKey(getPub.PublicKey) 32 | if err != nil { 33 | return nil, errors.WithStack(err) 34 | } 35 | 36 | return &signer{api: api, keyId: keyId, pub: pub}, nil 37 | } 38 | 39 | func (k *signer) Public() crypto.PublicKey { 40 | return k.pub 41 | } 42 | 43 | func (k *signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { 44 | sign, err := k.api.Sign(&kms.SignInput{ 45 | KeyId: &k.keyId, 46 | Message: digest, 47 | MessageType: aws.String(kms.MessageTypeDigest), 48 | SigningAlgorithm: aws.String(signatureMap[opts.HashFunc()]), 49 | }) 50 | if err != nil { 51 | return nil, errors.WithStack(err) 52 | } 53 | 54 | return sign.Signature, nil 55 | } 56 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "fmt" 9 | "github.com/aidansteele/flowdog/examples/account_id_emf" 10 | "github.com/aidansteele/flowdog/examples/geneve_headers" 11 | "github.com/aidansteele/flowdog/examples/lambda_acceptor" 12 | "github.com/aidansteele/flowdog/examples/sts_rickroll" 13 | "github.com/aidansteele/flowdog/examples/upsidedown" 14 | "github.com/aidansteele/flowdog/examples/webfirehose" 15 | "github.com/aidansteele/flowdog/gwlb" 16 | "github.com/aidansteele/flowdog/gwlb/mirror" 17 | "github.com/aidansteele/flowdog/gwlb/shark" 18 | "github.com/aidansteele/flowdog/kmssigner" 19 | "github.com/aidansteele/flowdog/mytls" 20 | "github.com/aws/aws-sdk-go/aws/session" 21 | "github.com/aws/aws-sdk-go/service/ec2" 22 | "github.com/aws/aws-sdk-go/service/firehose" 23 | "github.com/aws/aws-sdk-go/service/kms" 24 | "github.com/aws/aws-sdk-go/service/kms/kmsiface" 25 | "github.com/aws/aws-sdk-go/service/lambda" 26 | _ "github.com/google/gopacket/layers" 27 | "github.com/pkg/errors" 28 | "net" 29 | "net/http" 30 | "os" 31 | "time" 32 | ) 33 | 34 | func main() { 35 | ctx := context.Background() 36 | 37 | sess, err := session.NewSessionWithOptions(session.Options{ 38 | SharedConfigState: session.SharedConfigEnable, 39 | }) 40 | if err != nil { 41 | fmt.Printf("%+v\n", err) 42 | panic(err) 43 | } 44 | 45 | err = setupTls(kms.New(sess)) 46 | if err != nil { 47 | fmt.Printf("%+v\n", err) 48 | panic(err) 49 | } 50 | 51 | conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 6081}) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | wf := webfirehose.New(firehose.New(sess), ec2.New(sess), "webfirehose-Firehose-0k2UpKEJfMz0") 57 | go func() { 58 | err := wf.Run(ctx) 59 | if err != nil { 60 | fmt.Printf("%+v\n", err) 61 | panic(err) 62 | } 63 | }() 64 | 65 | chain := gwlb.Chain{ 66 | wf, 67 | &geneve_headers.GeneveHeaders{}, 68 | &account_id_emf.AccountIdEmf{}, 69 | &sts_rickroll.StsRickroll{}, 70 | upsidedown.UpsideDown(), 71 | //cloudfront_functions.NewRickroll(), 72 | } 73 | 74 | acceptor, err := lambda_acceptor.New( 75 | lambda.New(sess), 76 | os.Getenv("LAMBDA_ACCEPTOR_ARN"), 77 | ec2.New(sess), 78 | ) 79 | if err != nil { 80 | fmt.Printf("%+v\n", err) 81 | panic(err) 82 | } 83 | 84 | ctx := context.Background() 85 | 86 | shark := shark.NewSharkServer() 87 | sharkL, err := net.Listen("tcp", "127.0.0.1:7081") 88 | if err != nil { 89 | fmt.Printf("%+v\n", err) 90 | panic(err) 91 | } 92 | 93 | mirrorCh := make(chan mirror.Packet) 94 | go shark.Serve(ctx, sharkL, mirrorCh) 95 | 96 | server := &gwlb.Server{ 97 | Handler: gwlb.DefaultHandler(chain, &tls.Config{KeyLogWriter: shark.KeyLogWriter()}), 98 | Acceptor: acceptor, 99 | KeyLogWriter: shark.KeyLogWriter(), 100 | Mirror: mirrorCh, 101 | } 102 | 103 | go healthChecks() 104 | 105 | err = server.Serve(ctx, conn) 106 | if err != nil { 107 | fmt.Printf("%+v\n", err) 108 | panic(err) 109 | } 110 | } 111 | 112 | func setupTls(kmsapi kmsiface.KMSAPI) error { 113 | // TODO: env var or something 114 | certBlock, _ := pem.Decode([]byte(` 115 | -----BEGIN CERTIFICATE----- 116 | MIIBfjCCASSgAwIBAgIBATAKBggqhkjOPQQDAjAcMRowGAYDVQQDExFBaWRhbiBL 117 | TVMgVExTIENvLjAeFw0yMjAxMTEwMzU5NTBaFw0zMjAxMTEwMzU5NTBaMBwxGjAY 118 | BgNVBAMTEUFpZGFuIEtNUyBUTFMgQ28uMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD 119 | QgAE7nApN4JApU+uocRZiefgFoHwpeTTHZkcqVUUTfJp67GmmV4WJ2MTXAH2W+MP 120 | 2M8d5cHmgcrSAi6vIzLjvRZBuKNXMFUwDgYDVR0PAQH/BAQDAgKEMBMGA1UdJQQM 121 | MAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIAvKHJ7WiTA 122 | vlpCx+VMI/+lwgfuMAoGCCqGSM49BAMCA0gAMEUCIFwVxZTneQupaLH2Cunk7FdE 123 | nca45vDEVkjEZw7ERb7SAiEA5Sv3PbIBQSGihtWG4SOJ4tm8US29wM81w9Okl0vR 124 | qpw= 125 | -----END CERTIFICATE----- 126 | `)) 127 | 128 | cert, err := x509.ParseCertificate(certBlock.Bytes) 129 | if err != nil { 130 | return errors.WithStack(err) 131 | } 132 | 133 | signer, err := kmssigner.New(kmsapi, os.Getenv("KEY_ID")) 134 | if err != nil { 135 | return errors.WithStack(err) 136 | } 137 | 138 | return mytls.SetIntermediateCA( 139 | os.Getenv("INTERMEDIATE_CA_NAME"), 140 | time.Now().AddDate(1, 0, 0), 141 | cert, 142 | signer, 143 | ) 144 | } 145 | 146 | func healthChecks() { 147 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 148 | fmt.Println("health checks") 149 | w.Write([]byte("ok")) 150 | }) 151 | 152 | err := http.ListenAndServe(":8080", nil) 153 | if err != nil { 154 | fmt.Printf("%+v\n", err) 155 | panic(err) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /mytls/intermediate.go: -------------------------------------------------------------------------------- 1 | package mytls 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "github.com/pkg/errors" 11 | "math/big" 12 | "time" 13 | ) 14 | 15 | var ( 16 | intermediateCert *x509.Certificate 17 | intermediateSigner crypto.Signer 18 | ) 19 | 20 | func SetIntermediateCA(commonName string, expiry time.Time, caCertificate *x509.Certificate, signer crypto.Signer) error { 21 | limit := (&big.Int{}).Lsh(big.NewInt(1), 128) 22 | serialNumber, err := rand.Int(rand.Reader, limit) 23 | if err != nil { 24 | return errors.WithStack(err) 25 | } 26 | 27 | tmpl := x509.Certificate{ 28 | Subject: pkix.Name{CommonName: commonName}, 29 | SerialNumber: serialNumber, 30 | NotBefore: time.Now(), 31 | NotAfter: expiry, 32 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 33 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 34 | BasicConstraintsValid: true, 35 | IsCA: true, 36 | } 37 | 38 | intermediateSigner, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 39 | if err != nil { 40 | return errors.WithStack(err) 41 | } 42 | 43 | derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, caCertificate, intermediateSigner.Public(), signer) 44 | if err != nil { 45 | return errors.WithStack(err) 46 | } 47 | 48 | intermediateCert, err = x509.ParseCertificate(derBytes) 49 | if err != nil { 50 | return errors.WithStack(err) 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /mytls/tls_config.go: -------------------------------------------------------------------------------- 1 | package mytls 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "github.com/pkg/errors" 11 | "math/big" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | var certificateCache map[string]*tls.Certificate 17 | var certificateCacheLock sync.Mutex 18 | 19 | func TlsConfig() *tls.Config { 20 | return &tls.Config{ 21 | GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { 22 | certificateCacheLock.Lock() 23 | defer certificateCacheLock.Unlock() 24 | 25 | domain := info.ServerName 26 | 27 | if certificateCache == nil { 28 | certificateCache = map[string]*tls.Certificate{} 29 | } 30 | 31 | cert := certificateCache[domain] 32 | if cert != nil { 33 | return cert, nil 34 | } 35 | 36 | cert, err := generateTlsCertificate(domain) 37 | certificateCache[domain] = cert 38 | return cert, err 39 | }, 40 | } 41 | } 42 | 43 | func generateTlsCertificate(domain string) (*tls.Certificate, error) { 44 | limit := (&big.Int{}).Lsh(big.NewInt(1), 128) 45 | serialNumber, _ := rand.Int(rand.Reader, limit) 46 | 47 | tmpl := x509.Certificate{ 48 | Subject: pkix.Name{CommonName: domain}, 49 | DNSNames: []string{domain}, 50 | SerialNumber: serialNumber, 51 | NotBefore: time.Now(), 52 | NotAfter: time.Now().AddDate(1, 0, 0), 53 | KeyUsage: x509.KeyUsageDigitalSignature, 54 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 55 | BasicConstraintsValid: true, 56 | } 57 | 58 | key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 59 | if err != nil { 60 | return nil, errors.WithStack(err) 61 | } 62 | 63 | derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, intermediateCert, key.Public(), intermediateSigner) 64 | if err != nil { 65 | return nil, errors.WithStack(err) 66 | } 67 | 68 | return &tls.Certificate{ 69 | Certificate: [][]byte{derBytes, intermediateCert.Raw}, 70 | PrivateKey: key, 71 | }, nil 72 | } 73 | --------------------------------------------------------------------------------