Skip to content

Commit

Permalink
Merge pull request #146 from fastly/dgryski/acl
Browse files Browse the repository at this point in the history
acl: add ACL hostcalls
  • Loading branch information
dgryski authored Dec 3, 2024
2 parents c2c6c53 + 4ce85fb commit 4dca515
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Added

- secretstore: add Plaintext toplevel convenience function
- acl: add ACL hostcalls

## 1.3.3 (2024-09-12)

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions _examples/acl/acls.json
Original file line number Diff line number Diff line change
@@ -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" }
]
}
17 changes: 17 additions & 0 deletions _examples/acl/fastly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file describes a Fastly Compute package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = ["[email protected]"]
description = ""
language = "go"
manifest_version = 2
name = "gacls"



[scripts]
build = "tinygo build -target=wasip1 -o bin/main.wasm ./"


[local_server]
acls.example = "./acls.json"
41 changes: 41 additions & 0 deletions _examples/acl/main.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
115 changes: 115 additions & 0 deletions acl/acl.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions integration_tests/acl/acls.json
Original file line number Diff line number Diff line change
@@ -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" }
]
}
17 changes: 17 additions & 0 deletions integration_tests/acl/fastly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file describes a Fastly Compute package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = ["[email protected]"]
description = ""
language = "go"
manifest_version = 2
name = "gacls"



[scripts]
build = "tinygo build -target=wasip1 -o bin/main.wasm ./"


[local_server]
acls.example = "./acls.json"
49 changes: 49 additions & 0 deletions integration_tests/acl/main_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
101 changes: 101 additions & 0 deletions internal/abi/fastly/acl_guest.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions internal/abi/fastly/hostcalls_noguest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Loading

0 comments on commit 4dca515

Please sign in to comment.