Hello Friend

Overview⌗

API Payload capture is a key tool for troubleshooting.

Tcpdump is the tool working at ISO layer 3~6, Many modern API working at Layer 7(HTTP), many capture rules are working only on Layer 7. E.g. filter payload by URL Path is hard for tcpdump:

tcpdump -i enp0s8 -s 0 -v -n -l | egrep -i "POST /|GET /|Host:"

Old school: tcpdump on minikube(FT) or Node local⌗

Below is an example of tcpdump.

[root@host-os ~]# ps -ef | grep -i myServicePS
100       398212  398058  0 Apr07 ?        00:26:08 java ...myServicePS...

set CONTAINER_PID_AT_HOST=398212

[root@host-os ~]# nsenter -t $CONTAINER_PID_AT_HOST  -u -i -n -p -C tcpdump -A -vv
...

In many cases, it is hard to find your API in the network flood from tcpdump.

Sidecar(Envoy) Filter/Tap⌗

In Istio(Service Mesh), Envoy is the sidecar. All API traffic proxy by it. It supports HTTP Filters, we can create some filter on Envoy to capture payload.

Two methods are supported:

  • Tap Fitler
  • Lua Filter

Setup⌗

kubectl apply -f istio-envoy-log-filter.yaml

#Optional: check k8s resource: 
kubectl describe envoyfilters.networking.istio.io logpayload

Optional: check envoy configuartion by envoy admin port 15000:

kubectl port-forward $your_pod_name 15000:15000
curl localhost:15000/config_dump | grep -i logPayload

You can download istio-envoy-log-filter.yaml here. workloadSelector and namespace is needed to updated for your case.

Capture⌗

Method 1: Capture by Tap Fitler⌗

Make sure we have port forward to envoy:

kubectl port-forward $your_pod_name 15000:15000

curl long poll:

curl -XPOST -d 'config_id: test_config_id
tap_config:
  match_config:
    any_match: true
  output_config:
    sinks:
      - format: JSON_BODY_AS_STRING
        streaming_admin: {}
    max_buffered_rx_bytes: 2097152      
    max_buffered_tx_bytes: 2097152' 'http://localhost:15000/tap'

Open another terminal, access an API of your application, e.g:

curl http://your-service:your-service-port/your-api-path

The ‘curl long poll’ return the API payload like this:

{
 "http_buffered_trace": {
  "request": {
   "headers": [
    {
     "key": ":path",
     "value": "/productpage"
    },
    {
     "key": ":method",
     "value": "GET"
    },
    {
     "key": ":scheme",
     "value": "http"
    },
    {
     "key": "user-agent",
     "value": "curl/7.64.1"
    },
    {
     "key": "accept",
     "value": "*/*"
    },
    {
     "key": "x-forwarded-for",
     "value": "10.244.1.1"
    }
   ],
   "trailers": []
  },
  "response": {
   "headers": [
    {
     "key": ":status",
     "value": "200"
    },
    {
     "key": "content-type",
     "value": "text/html; charset=utf-8"
    },
    {
     "key": "content-length",
     "value": "3889"
    },
    {
     "key": "server",
     "value": "istio-envoy"
    },
    {
     "key": "date",
     "value": "Sun, 12 Apr 2020 01:07:40 GMT"
    },
    {
     "key": "x-envoy-upstream-service-time",
     "value": "192"
    },
    {
     "key": "x-envoy-peer-metadata-id",
     "value": "sidecar~10.244.2.37~productpage-v1-5f7b7d4568-hmqzd.default~default.svc.cluster.local"
    }
   ],
   "body": {
    "truncated": false,
    "as_string": "\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003eSimple Bookstore App\u003c/titleon('shown.bs.modal', function () {\n    $('#username').focus();\n  });\n\u003c/script\u003e\n\n  \u003c/body\u003e\n\u003c/html\u003e\n"
   },
   "trailers": []
  }
 }
}

Method 2: By kubectl logs⌗

Open another terminal, access an API of your application, e.g:

curl http://your-service:your-service-port/your-api-path

View the log of the sidecar(Envoy):

kubectl logs -f your-pod -c istio-proxy

The paylod in the log:

[Envoy (Epoch 0)] [2020-04-12 01:07:41.292][29][warning][lua] [external/envoy/source/extensions/filters/http/lua/lua_filter.cc:597] script log: <!DOCTYPE html>
<html>
  <head>
    <title>Simple Bookstore App</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

How it work⌗

Let’s have a look at what in kubectl apply -f istio-envoy-log-filter.yaml :

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: logpayload
  namespace: default
spec:
  workloadSelector:
    labels:
      app: productpage
  configPatches:
    # The first patch adds the lua filter to the listener/http connection manager
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
#        portNumber: 15001
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
            subFilter:
              name: "envoy.router"
    patch:
      operation: INSERT_BEFORE
      value: # filter specification
        # name: envoy.filters.http.tap
        #   config:
        #     common_config:
        #       static_config:
        #         match_config:
        #           any_match: true
        #         output_config:
        #           sinks:
        #             - file_per_tap:
        #                 path_prefix: taps/any

        name: "envoy.filters.http.tap"
        config:
          common_config:
            admin_config:
              config_id: test_config_id

  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
#        portNumber: 15001
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
            subFilter:
              name: "envoy.router"
    patch:
      operation: INSERT_BEFORE
      value: # filter specification
          name: envoy.lua
          typed_config:
            "@type": type.googleapis.com/envoy.config.filter.http.lua.v2.Lua
            inline_code: |
              function logPayload(handle)
                  handle:logWarn("[logPayload] started")

                  local index = 0
                  for chunk in handle:bodyChunks() do
                      handle:logWarn('[logPayload] showing bodyChunks')

                      local len = chunk:length()
                      if len < 1 then
                        break
                      end
                      local result = chunk:getBytes(index, len)
                      index = index + len

                      handle:logWarn(result)
                  end

                  handle:logWarn("[logPayload] finished")
              end            

              function envoy_on_request(handle)
                logPayload(handle)
              end
              function envoy_on_response(handle)
                logPayload(handle)
              end   

First, Envoy Filter:

Second, How Istio manage the envoy filter resource:

How to customize⌗

filter by service/deployment/replica⌗

update istio-envoy-log-filter.yaml :

spec:
  workloadSelector:
    labels:
      app: your-app-label
kubectl apply -f istio-envoy-log-filter.yaml

More info: https://istio.io/docs/reference/config/networking/envoy-filter/

filter by URL⌗

When capture by Tap:⌗
curl -XPOST -d 'config_id: test_config_id
tap_config:
  match_config:
    http_request_headers_match:
       headers:
        - name: ":path"
          exact_match: "/productpage"
        #- name: ":path"
          #safe_regex_match: 
          #  google_re2: 
          #    max_program_size: 200
          #  regex: ".*productpage.*"
        #- name: ":method"
        #  exact_match: "POST"
  output_config:
    sinks:
      - format: JSON_BODY_AS_STRING
        streaming_admin: {}
    max_buffered_rx_bytes: 2097152      
    max_buffered_tx_bytes: 2097152' 'http://localhost:15000/tap' 
When capture by k8s log:⌗
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: logpayload
  namespace: default
spec:
  workloadSelector:
    labels:
      app: productpage
  configPatches:
    # The first patch adds the lua filter to the listener/http connection manager

  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
#        portNumber: 15001
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
            subFilter:
              name: "envoy.router"
    patch:
      operation: INSERT_BEFORE
      value: # filter specification
          name: envoy.lua
          typed_config:
            "@type": type.googleapis.com/envoy.config.filter.http.lua.v2.Lua
            inline_code: |
              function logPayload(handle)
                    handle:logWarn("[logPayload] started")

                    local headStr = ""
                    for key, value in pairs(handle:headers()) do
                      headStr = headStr .. "\n" .. key .. ":" .. value .. ","
                    end             
                    
                    handle:logWarn("[logPayload:headers] --> " .. headStr)

                    local index = 0
                    for chunk in handle:bodyChunks() do
                        handle:logWarn('[logPayload] showing bodyChunks')

                        local len = chunk:length()
                        if len < 1 then
                          break
                        end
                        local result = chunk:getBytes(index, len)
                        index = index + len

                        handle:logWarn(result)
                    end
              end            

              function envoy_on_request(handle)
                  local path = handle:headers():get(":path")
                  if path ~= nil and path:find("productpage") ~= nil then
                    handle:streamInfo():dynamicMetadata():set("envoy.lua", "request.info", "true")                    
                    logPayload( handle )
                  end              
              end

              function envoy_on_response(handle)
                local path = handle:headers():get(":path")
                if path ~= nil and path:find("productpage") ~= nil then
                  handle:logWarn("[envoy_on_response] find productpage")
                end

                if handle:streamInfo():dynamicMetadata():get("envoy.lua") == nil then
                  return
                end

                local meta = handle:streamInfo():dynamicMetadata():get("envoy.lua")["request.info"]
                if( meta ~= nil ) then
                    handle:logWarn("[envoy_on_request] meta logPayload found")
                    logPayload( handle )
                end
              end

You can download istio-envoy-log-filter-by-path.yaml here

Tap to file⌗

TODO

Clean up⌗

kubectl delete -f istio-envoy-log-filter.yaml

Ref.⌗