-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #146 from fastly/dgryski/acl
acl: add ACL hostcalls
- Loading branch information
Showing
12 changed files
with
413 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.