From 4e59a194b8de0721bb16413ed73cdd6c6acc8d18 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Fri, 15 Nov 2024 12:42:52 -0800 Subject: [PATCH 1/4] acl: add ACL hostcalls --- acl/acl.go | 115 +++++++++++++++++++++++ internal/abi/fastly/acl_guest.go | 101 ++++++++++++++++++++ internal/abi/fastly/hostcalls_noguest.go | 10 ++ internal/abi/fastly/types.go | 44 +++++++++ 4 files changed, 270 insertions(+) create mode 100644 acl/acl.go create mode 100644 internal/abi/fastly/acl_guest.go diff --git a/acl/acl.go b/acl/acl.go new file mode 100644 index 0000000..f087e67 --- /dev/null +++ b/acl/acl.go @@ -0,0 +1,115 @@ +// Package acl provides access to Fastly ACLs. +// +// See the [Fastly ACL documentation] for details. +// +// [Fastly ACL documentation]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#access-control-lists +package acl + +import ( + "encoding/json" + "errors" + "fmt" + "net" + + "github.com/fastly/compute-sdk-go/internal/abi/fastly" +) + +var ( + // ErrNotFound indicates the requested ACL was not found. + ErrNotFound = errors.New("acl: not found") + + // ErrInvalidHandle indicatest the ACL handle was invalid. + ErrInvalidHandle = errors.New("acl: invalid handle") + + // ErrInvalidResponseBody indicates the looup response body was invalid. + ErrInvalidResponseBody = errors.New("acl: invalid response body") + + // ErrInvalidArgument indicates the IP address was invalid. + ErrInvalidArgument = errors.New("acl: invalid argument") + + // ErrNoContent indicates there was no entry for the provided IP address. + ErrNoContent = errors.New("acl: no content") + + // ErrTooManyRequests indicates too many requests were made. + ErrTooManyRequests = errors.New("acl: too many requests") + + // ErrUnexpected indicates an unexpected error occurred. + ErrUnexpected = errors.New("acl: unexepected error") +) + +// Handle is a handle for an ACL +type Handle struct { + h *fastly.ACLHandle +} + +// Response is an ACL lookup response +type Response struct { + Prefix string // Matching prefix in CIDR notation + Action string // Associated prefix's action +} + +// Open returns a handle to the named ACL. +func Open(name string) (*Handle, error) { + a, err := fastly.OpenACL(name) + if err != nil { + status, ok := fastly.IsFastlyError(err) + switch { + case ok && status == fastly.FastlyStatusNone: + return nil, ErrNotFound + case ok: + return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status) + default: + return nil, err + } + } + + return &Handle{h: a}, nil + +} + +// Lookup the given IP in the ACL and returns the response. If no match was found, returns ErrNoContent. +func (h *Handle) Lookup(ip net.IP) (Response, error) { + body, err := h.h.Lookup(ip) + if err != nil { + return Response{}, mapFastlyErr(err) + } + + var r Response + dec := json.NewDecoder(body) + if err := dec.Decode(&r); err != nil { + return Response{}, err + } + return r, nil +} + +func mapFastlyErr(err error) error { + // Is it a acl-specific error? + if aclErr, ok := err.(fastly.ACLError); ok { + switch aclErr { + case fastly.ACLErrorUninitialized: // we really shouldn't be returning this + return ErrUnexpected + case fastly.ACLErrorOK: + // Not an error; we shouldn't get here + return fmt.Errorf("%w (%s)", ErrUnexpected, err) + case fastly.ACLErrorNoContent: + return ErrNoContent + case fastly.ACLErrorTooManyRequests: + return ErrTooManyRequests + } + return fmt.Errorf("%w (%s)", ErrUnexpected, err) + } + + // Maybe it was a fastly error? + status, ok := fastly.IsFastlyError(err) + switch { + case ok && status == fastly.FastlyStatusBadf: + return ErrInvalidHandle + case ok && status == fastly.FastlyStatusInval: + return ErrInvalidArgument + case ok: + return fmt.Errorf("%w (%s)", ErrUnexpected, status) + } + + // No idea; just return what we have. + return err +} diff --git a/internal/abi/fastly/acl_guest.go b/internal/abi/fastly/acl_guest.go new file mode 100644 index 0000000..ae6bbd4 --- /dev/null +++ b/internal/abi/fastly/acl_guest.go @@ -0,0 +1,101 @@ +//go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls + +// Copyright 2024 Fastly, Inc. + +package fastly + +import ( + "io" + "net" + + "github.com/fastly/compute-sdk-go/internal/abi/prim" +) + +// witx: +// +// (@interface func (export "open") +// (param $name string) +// (result $err (expected $acl_handle (error $fastly_status))) +// ) + +//go:wasmimport fastly_acl open +//go:noescape +func fastlyACLOpen( + nameData prim.Pointer[prim.U8], nameLen prim.Usize, + h prim.Pointer[aclHandle], +) FastlyStatus + +// ACL is a handle to the ACL subsystem. +type ACLHandle struct { + h aclHandle +} + +// OpenACL returns a handle to the named ACL set. +func OpenACL(name string) (*ACLHandle, error) { + var acl ACLHandle + + nameBuffer := prim.NewReadBufferFromString(name).Wstring() + + if err := fastlyACLOpen( + nameBuffer.Data, nameBuffer.Len, + prim.ToPointer(&acl.h), + ).toError(); err != nil { + return nil, err + } + + return &acl, nil +} + +// witx: +// +// (@interface func (export "lookup") +// (param $acl $acl_handle) +// (param $ip_octets (@witx const_pointer (@witx char8))) +// (param $ip_len (@witx usize)) +// (param $body_handle_out (@witx pointer $body_handle)) +// (param $acl_error_out (@witx pointer $acl_error)) +// (result $err (expected (error $fastly_status))) +// ) + +//go:wasmimport fastly_acl lookup +//go:noescape +func fastlyACLLookup( + h aclHandle, + ipData prim.Pointer[prim.U8], ipLen prim.Usize, + b prim.Pointer[bodyHandle], + aclErr prim.Pointer[ACLError], +) FastlyStatus + +// Lookup returns the entry for the IP, if it exists. +func (a *ACLHandle) Lookup(ip net.IP) (io.Reader, error) { + body := HTTPBody{h: invalidBodyHandle} + + var ipBytes []byte + if ipBytes = ip.To4(); ipBytes == nil { + ipBytes = ip.To16() + } + ipBuffer := prim.NewReadBufferFromBytes(ipBytes).ArrayChar8() + + var aclErr ACLError = ACLErrorUninitialized + + if err := fastlyACLLookup( + a.h, + ipBuffer.Data, ipBuffer.Len, + prim.ToPointer(&body.h), + prim.ToPointer(&aclErr), + ).toError(); err != nil { + return nil, err + } + + if aclErr != ACLErrorOK { + return nil, aclErr + } + + // Didn't get a valid handle back. This means there was no matching + // ACL prefix. Report back to caller we got no match. + if body.h == invalidBodyHandle { + return nil, ACLErrorNoContent + } + + return &body, nil +} diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index 8eb8142..95b175b 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -558,3 +558,13 @@ func PenaltyBoxHas(penaltyBox, entry string) (bool, error) { func GetVCPUMilliseconds() (uint64, error) { return 0, fmt.Errorf("not implemented") } + +type ACLHandle struct{} + +func OpenACL(name string) (*ACLHandle, error) { + return nil, fmt.Errorf("not implemented") +} + +func (acl *ACLHandle) Lookup(ip net.IP) (*HTTPBody, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/internal/abi/fastly/types.go b/internal/abi/fastly/types.go index 1d17330..40ed8a5 100644 --- a/internal/abi/fastly/types.go +++ b/internal/abi/fastly/types.go @@ -1464,3 +1464,47 @@ var ( CounterDuration50s = CounterDuration{value: 50} CounterDuration60s = CounterDuration{value: 60} ) + +// witx: +// +// ;;; A handle to an ACL. +// (typename $acl_handle (handle)) +type aclHandle handle + +type ACLError prim.U32 + +// witx: +// +// (enum (@witx tag u32) +// ;;; The $acl_error has not been initialized. +// $uninitialized +// ;;; There was no error. +// $ok +// ;;; This will map to the api's 204 code. +// ;;; It indicates that the request succeeded, yet returned nothing. +// $no_content +// ;;; This will map to the api's 429 code. +// ;;; Too many requests have been made. +// $too_many_requests +// )) +const ( + ACLErrorUninitialized ACLError = 0 + ACLErrorOK ACLError = 1 + ACLErrorNoContent ACLError = 2 + ACLErrorTooManyRequests ACLError = 3 +) + +func (e ACLError) Error() string { + switch e { + case ACLErrorUninitialized: + return "uninitialized" + case ACLErrorOK: + return "ok" + case ACLErrorNoContent: + return "no content" + case ACLErrorTooManyRequests: + return "too many requests" + } + + return "unknown" +} From 8f58a083186b6084d0b55b2b811d7edb34a3cef9 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Fri, 15 Nov 2024 15:21:06 -0800 Subject: [PATCH 2/4] integration_tests/acl: ACL integration test --- integration_tests/acl/acls.json | 8 +++++ integration_tests/acl/fastly.toml | 17 +++++++++++ integration_tests/acl/main_test.go | 49 ++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 integration_tests/acl/acls.json create mode 100644 integration_tests/acl/fastly.toml create mode 100644 integration_tests/acl/main_test.go diff --git a/integration_tests/acl/acls.json b/integration_tests/acl/acls.json new file mode 100644 index 0000000..ee44a2e --- /dev/null +++ b/integration_tests/acl/acls.json @@ -0,0 +1,8 @@ +{ + "entries": [ + { "op": "update", "prefix": "1.2.3.0/24", "action": "BLOCK" }, + { "op": "create", "prefix": "192.168.0.0/16", "action": "BLOCK" }, + { "op": "update", "prefix": "23.23.23.23/32", "action": "ALLOW" }, + { "op": "create", "prefix": "1.2.3.4/32", "action": "ALLOW" } + ] +} diff --git a/integration_tests/acl/fastly.toml b/integration_tests/acl/fastly.toml new file mode 100644 index 0000000..5216449 --- /dev/null +++ b/integration_tests/acl/fastly.toml @@ -0,0 +1,17 @@ +# This file describes a Fastly Compute package. To learn more visit: +# https://developer.fastly.com/reference/fastly-toml/ + +authors = ["oss@fastly.com"] +description = "" +language = "go" +manifest_version = 2 +name = "gacls" + + + +[scripts] +build = "tinygo build -target=wasip1 -o bin/main.wasm ./" + + +[local_server] +acls.example = "./acls.json" diff --git a/integration_tests/acl/main_test.go b/integration_tests/acl/main_test.go new file mode 100644 index 0000000..843165d --- /dev/null +++ b/integration_tests/acl/main_test.go @@ -0,0 +1,49 @@ +//go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls + +// Copyright 2023 Fastly, Inc. + +package main + +import ( + "errors" + "net" + "testing" + + "github.com/fastly/compute-sdk-go/acl" +) + +func TestACL(t *testing.T) { + store, err := acl.Open("example") + if err != nil { + t.Fatal(err) + } + + var tests = []struct { + ip string + r acl.Response + err error + }{ + {"1.2.3.4", acl.Response{Prefix: "1.2.3.4/32", Action: "ALLOW"}, nil}, + {"1.1.1.1", acl.Response{}, acl.ErrNoContent}, + {"1.1.1", acl.Response{}, acl.ErrInvalidArgument}, + } + + for _, tt := range tests { + + lookup, err := store.Lookup(net.ParseIP(tt.ip)) + if (tt.err == nil && err != nil) || (tt.err != nil && !errors.Is(err, tt.err)) { + t.Errorf("Lookup(%v) error mismatch: got %v, want %v", tt.ip, err, tt.err) + continue + } + + if lookup.Prefix != tt.r.Prefix || lookup.Action != tt.r.Action { + t.Errorf("Lookup(%v) mismatch: got %#v, want %#v\n", tt.ip, lookup, tt.r) + } + + } + + store, err = acl.Open("does-not-exist") + if err != acl.ErrNotFound { + t.Errorf("Open(does-not-exist) err = %v, want %v\n", err, acl.ErrNotFound) + } +} From d173750544ff14ddb44b1dd0018a1c8520f69585 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Fri, 15 Nov 2024 15:25:52 -0800 Subject: [PATCH 3/4] _examples/acl: ACL example program --- README.md | 4 ++-- _examples/acl/acls.json | 8 ++++++++ _examples/acl/fastly.toml | 17 ++++++++++++++++ _examples/acl/main.go | 41 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 _examples/acl/acls.json create mode 100644 _examples/acl/fastly.toml create mode 100644 _examples/acl/main.go diff --git a/README.md b/README.md index c1664d5..e078fd4 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,11 @@ The `fastly-compute.json` file provides a TinyGo target to run Viceroy. (In the To run your tests: - tinygo test -target=fastly-compute.json ./... + tinygo test -target=targets/fastly-compute-wasip1.json ./... You can try it out and make sure your local Viceroy environment is set up correctly by running the integration tests in this repository: - tinygo test -target=fastly-compute.json ./integration_tests/... + tinygo test -target=targets/fastly-compute-wasip1.json ./integration_tests/... ### Go diff --git a/_examples/acl/acls.json b/_examples/acl/acls.json new file mode 100644 index 0000000..ee44a2e --- /dev/null +++ b/_examples/acl/acls.json @@ -0,0 +1,8 @@ +{ + "entries": [ + { "op": "update", "prefix": "1.2.3.0/24", "action": "BLOCK" }, + { "op": "create", "prefix": "192.168.0.0/16", "action": "BLOCK" }, + { "op": "update", "prefix": "23.23.23.23/32", "action": "ALLOW" }, + { "op": "create", "prefix": "1.2.3.4/32", "action": "ALLOW" } + ] +} diff --git a/_examples/acl/fastly.toml b/_examples/acl/fastly.toml new file mode 100644 index 0000000..5216449 --- /dev/null +++ b/_examples/acl/fastly.toml @@ -0,0 +1,17 @@ +# This file describes a Fastly Compute package. To learn more visit: +# https://developer.fastly.com/reference/fastly-toml/ + +authors = ["oss@fastly.com"] +description = "" +language = "go" +manifest_version = 2 +name = "gacls" + + + +[scripts] +build = "tinygo build -target=wasip1 -o bin/main.wasm ./" + + +[local_server] +acls.example = "./acls.json" diff --git a/_examples/acl/main.go b/_examples/acl/main.go new file mode 100644 index 0000000..4c50830 --- /dev/null +++ b/_examples/acl/main.go @@ -0,0 +1,41 @@ +// Copyright 2024 Fastly, Inc. + +package main + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/fastly/compute-sdk-go/acl" + "github.com/fastly/compute-sdk-go/fsthttp" +) + +func main() { + fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { + aclh, err := acl.Open("example") + if err != nil { + fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) + return + } + + ip := r.URL.Query().Get("ip") + if ip == "" { + ip = r.RemoteAddr + } + + netip := net.ParseIP(ip) + aclr, err := aclh.Lookup(netip) + if errors.Is(err, acl.ErrNoContent) { + fmt.Fprintln(w, "IP:", ip, "No Match") + return + } + if err != nil { + fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError) + return + } + + fmt.Fprintln(w, "IP:", ip, "Prefix:", aclr.Prefix, "Action:", aclr.Action) + }) +} From 4ce85fbb963b168b602f0ec6cacd90aa5db1ebff Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Fri, 15 Nov 2024 15:32:08 -0800 Subject: [PATCH 4/4] CHANGELOG: update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index caaf29e..646b5f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added - secretstore: add Plaintext toplevel convenience function +- acl: add ACL hostcalls ## 1.3.3 (2024-09-12)