Skip to content

Commit

Permalink
Adds ProcessConnection and RuleEngineOff checks (envoyproxy#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
M4tteoP authored Dec 15, 2022
1 parent 94c8fb7 commit 19a223f
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 8 deletions.
104 changes: 104 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package main

import (
"bytes"
"encoding/binary"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -797,6 +799,108 @@ SecRuleEngine On\nSecRule REQUEST_URI \"@streq /hello\" \"id:101,phase:4,t:lower
})
}

func TestRetrieveAddressInfo(t *testing.T) {
var unsetPort = -1
reqHdrs := [][2]string{
{":path", "/hello"},
{":method", "GET"},
}
testCases := []struct {
name string
addressProperty string
portProperty int
expectIP string
expectPort int
requestHdrsAction types.Action
}{
{
name: "IPv4 parse, usual circumstances",
addressProperty: "127.0.0.1:5001",
portProperty: 50001,
expectIP: "127.0.0.1",
expectPort: 50001,
requestHdrsAction: types.ActionPause,
},
{
name: "IPv4 parse, port retrieved from address",
addressProperty: "127.0.0.1:5002",
portProperty: unsetPort,
expectIP: "127.0.0.1",
expectPort: 50002,
requestHdrsAction: types.ActionPause,
},
{
name: "IPv6 parse, usual circumstances",
addressProperty: "[2001:db8::1]:8001",
portProperty: 8001,
expectIP: "[2001:db8::1]",
expectPort: 8001,
requestHdrsAction: types.ActionPause,
},
{
name: "IPv6 parse, port retrieved from address",
addressProperty: "[2001:db8::1]:8002",
portProperty: unsetPort,
expectIP: "[2001:db8::1]",
expectPort: 8002,
requestHdrsAction: types.ActionPause,
},
{
name: "No properties retrieved, OnRequestHeaders does not fail",
addressProperty: "",
portProperty: unsetPort,
expectIP: "127.0.0.1",
expectPort: 80,
requestHdrsAction: types.ActionContinue,
},
}

vmTest(t, func(t *testing.T, vm types.VMContext) {

m := map[string]string{
"source": "REMOTE",
"destination": "SERVER",
}
for target, targetSecRuleVariable := range m {

for _, tc := range testCases {
tt := tc
inlineRules := fmt.Sprintf(`
SecRuleEngine On\nSecRule %s_ADDR \"@ipMatch %s\" \"id:101,phase:1,deny\"\nSecRule %s_PORT \"@eq %d\" \"id:102,phase:1,deny\"
`, targetSecRuleVariable, tt.expectIP, targetSecRuleVariable, tt.expectPort)

conf := `{}`
if inlineRules := strings.TrimSpace(inlineRules); inlineRules != "" {
conf = fmt.Sprintf(`{"rules": ["%s"]}`, inlineRules)
}
t.Run(tt.name, func(t *testing.T) {
opt := proxytest.
NewEmulatorOption().
WithVMContext(vm).
WithPluginConfiguration([]byte(conf))

host, reset := proxytest.NewHostEmulator(opt)
defer reset()

require.Equal(t, types.OnPluginStartStatusOK, host.StartPlugin())
id := host.InitializeHttpContext()

if tt.addressProperty != "" {
require.NoError(t, host.SetProperty([]string{target, "address"}, []byte(tt.addressProperty)))
}
if tt.portProperty != unsetPort {
buf := new(bytes.Buffer)
require.NoError(t, binary.Write(buf, binary.LittleEndian, uint64(tt.portProperty)))
require.NoError(t, host.SetProperty([]string{target, "port"}, buf.Bytes()))
}
action := host.CallOnRequestHeaders(id, reqHdrs, false)
require.Equal(t, tt.requestHdrsAction, action)
})
}
}
})
}

func vmTest(t *testing.T, f func(*testing.T, types.VMContext)) {
t.Helper()

Expand Down
93 changes: 85 additions & 8 deletions wasmplugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ package wasmplugin

import (
"bytes"
"encoding/binary"
"errors"
"math"
"net"
"strconv"
"strings"

Expand Down Expand Up @@ -110,6 +114,15 @@ func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) t
// and its request properties, but they may not be true of other proxies implementing
// proxy-wasm.

if tx.IsRuleEngineOff() {
return types.ActionContinue
}
// OnHttpRequestHeaders does not terminate if IP/Port retrieve goes wrong
srcIP, srcPort := retrieveAddressInfo("source")
dstIP, dstPort := retrieveAddressInfo("destination")

tx.ProcessConnection(srcIP, srcPort, dstIP, dstPort)

// Note the pseudo-header :path includes the query.
// See https://httpwg.org/specs/rfc9113.html#rfc.section.8.3.1
uri, err := proxywasm.GetHttpRequestHeader(":path")
Expand Down Expand Up @@ -163,6 +176,10 @@ func (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.
defer logTime("OnHttpRequestBody", currentTime())
tx := ctx.tx

if tx.IsRuleEngineOff() {
return types.ActionContinue
}

// Do not perform any action related to request body if SecRequestBodyAccess is set to false
if !tx.IsRequestBodyAccessible() {
proxywasm.LogDebug("skipping request body inspection, SecRequestBodyAccess is off.")
Expand Down Expand Up @@ -207,6 +224,10 @@ func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool)
defer logTime("OnHttpResponseHeaders", currentTime())
tx := ctx.tx

if tx.IsRuleEngineOff() {
return types.ActionContinue
}

// Requests without body won't call OnHttpRequestBody, but there are rules in the request body
// phase that still need to be executed. If they haven't been executed yet, now is the time.
if !ctx.processedRequestBody {
Expand Down Expand Up @@ -253,6 +274,10 @@ func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types
defer logTime("OnHttpResponseBody", currentTime())
tx := ctx.tx

if tx.IsRuleEngineOff() {
return types.ActionContinue
}

// Do not perform any action related to response body if SecResponseBodyAccess is set to false
if !tx.IsResponseBodyAccessible() {
proxywasm.LogDebug("skipping response body inspection, SecResponseBodyAccess is off.")
Expand Down Expand Up @@ -312,17 +337,21 @@ func (ctx *httpContext) OnHttpStreamDone() {
defer logTime("OnHttpStreamDone", currentTime())
tx := ctx.tx

// Responses without body won't call OnHttpResponseBody, but there are rules in the response body
// phase that still need to be executed. If they haven't been executed yet, now is the time.
if !ctx.processedResponseBody {
ctx.processedResponseBody = true
_, err := tx.ProcessResponseBody()
if err != nil {
proxywasm.LogCriticalf("failed to process response body: %v", err)
if !tx.IsRuleEngineOff() {
// Responses without body won't call OnHttpResponseBody, but there are rules in the response body
// phase that still need to be executed. If they haven't been executed yet, now is the time.
if !ctx.processedResponseBody {
ctx.processedResponseBody = true
_, err := tx.ProcessResponseBody()
if err != nil {
proxywasm.LogCriticalf("failed to process response body: %v", err)
}
}
}

// ProcessLogging is still called even if RuleEngine is off for potential logs generated before the engine is turned off.
// Internally, if the engine is off, no log phase rules are evaluated
ctx.tx.ProcessLogging()

_ = ctx.tx.Close()
proxywasm.LogInfof("%d finished", ctx.contextID)
logMemStats()
Expand Down Expand Up @@ -365,3 +394,51 @@ func logError(error ctypes.MatchedRule) {
proxywasm.LogDebug(msg)
}
}

// Retrieves adddress properties from the proxy
// Expected targets are "source" or "destination"
// Envoy ref: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes#connection-attributes
func retrieveAddressInfo(target string) (string, int) {
var targetIP, targetPortStr string
var targetPort int
srcAddressRaw, err := proxywasm.GetProperty([]string{target, "address"})
if err != nil {
proxywasm.LogWarnf("failed to get %s address: %v", target, err)
} else {
targetIP, targetPortStr, err = net.SplitHostPort(string(srcAddressRaw))
if err != nil {
proxywasm.LogWarnf("failed to parse %s address: %v", target, err)
}
}
srcPortRaw, err := proxywasm.GetProperty([]string{target, "port"})
if err != nil {
// If GetProperty fails we rely on the port inside the Address property
// Mostly useful for proxies other than Envoy
targetPort, err = strconv.Atoi(targetPortStr)
if err != nil {
proxywasm.LogInfof("failed to get %s port: %v", target, err)
}
} else {
targetPort, err = parsePort(srcPortRaw)
if err != nil {
proxywasm.LogWarnf("failed to parse %s port: %v", target, err)
}
}
return targetIP, targetPort
}

// Converts port, retrieved as little-endian bytes, into int
func parsePort(b []byte) (int, error) {
// Port attribute ({"source", "port"}) is populated as uint64 (8 byte)
// Ref: https://github.com/envoyproxy/envoy/blob/1b3da361279a54956f01abba830fc5d3a5421828/source/common/network/utility.cc#L201
if len(b) < 8 {
return 0, errors.New("port bytes not found")
}
// 0 < Port number <= 65535, therefore the retrieved value should never exceed 16 bits
// and correctly fit int (at least 32 bits in size)
unsignedInt := binary.LittleEndian.Uint64(b)
if unsignedInt > math.MaxInt32 {
return 0, errors.New("port convertion error")
}
return int(unsignedInt), nil
}

0 comments on commit 19a223f

Please sign in to comment.