ACL Reference

January 20, 2026 ยท View on GitHub

The ACL module provides IP-based access control list lookups.

Acl

Opening an ACL

pub fn open(name: []const u8) !Acl

Open an ACL by name.

const Acl = zigly.Acl;

var blocklist = try Acl.open("ip_blocklist");

Methods

lookup

pub fn lookup(self: Acl, ip: Ip) !LookupResult

Look up an IP address in the ACL. Returns a body with JSON result and an error code.

const result = try acl.lookup(client_ip);
defer result.body.close() catch {};

if (result.acl_error == .no_content) {
    // No match found
}

match

pub fn match(self: Acl, allocator: Allocator, ip: Ip) !?std.json.Parsed(MatchResult)

Look up an IP and get parsed result. Returns null if no match.

if (try acl.match(allocator, client_ip)) |result| {
    defer result.deinit();

    if (result.value.isBlock()) {
        // IP should be blocked
    } else if (result.value.isAllow()) {
        // IP is explicitly allowed
    }
} else {
    // No ACL match, apply default policy
}

LookupResult

Raw lookup result.

pub const LookupResult = struct {
    body: Body,
    acl_error: AclError,
};

AclError

ACL-specific error codes.

pub const AclError = enum(u32) {
    ok = 1,               // Successful lookup with match
    no_content = 2,       // No match found
    too_many_requests = 3, // Rate limited
};

MatchResult

Parsed ACL match.

pub const MatchResult = struct {
    action: []const u8,   // "ALLOW" or "BLOCK"
    prefix: []const u8,   // Matched prefix ("192.168.0.0/16")

    pub fn isBlock(self: MatchResult) bool;
    pub fn isAllow(self: MatchResult) bool;
};

Example Usage

IP Blocking

const std = @import("std");
const zigly = @import("zigly");
const Acl = zigly.Acl;

fn start() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    var downstream = try zigly.downstream();
    const client_ip = try zigly.http.Downstream.getClientIpAddr();

    var blocklist = try Acl.open("blocklist");

    if (try blocklist.match(allocator, client_ip)) |result| {
        defer result.deinit();

        if (result.value.isBlock()) {
            try downstream.response.setStatus(403);
            try downstream.response.body.writeAll("Access denied");
            try downstream.response.finish();
            return;
        }
    }

    try downstream.proxy("origin", null);
}

Allow List

fn start() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    var downstream = try zigly.downstream();
    const client_ip = try zigly.http.Downstream.getClientIpAddr();

    var allowlist = try Acl.open("admin_ips");

    var uri_buf: [4096]u8 = undefined;
    const uri = try downstream.request.getUriString(&uri_buf);

    // Protect admin paths
    if (std.mem.startsWith(u8, uri, "/admin/")) {
        const allowed = if (try allowlist.match(allocator, client_ip)) |result| blk: {
            defer result.deinit();
            break :blk result.value.isAllow();
        } else false;

        if (!allowed) {
            try downstream.response.setStatus(403);
            try downstream.response.finish();
            return;
        }
    }

    try downstream.proxy("origin", null);
}

Geo + ACL

fn start() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    var downstream = try zigly.downstream();
    const client_ip = try zigly.http.Downstream.getClientIpAddr();

    // Check ACL first
    var blocklist = try Acl.open("blocklist");
    if (try blocklist.match(allocator, client_ip)) |result| {
        defer result.deinit();
        if (result.value.isBlock()) {
            try downstream.response.setStatus(403);
            try downstream.response.finish();
            return;
        }
    }

    // Then check geo
    var geo_buf: [4096]u8 = undefined;
    const geo_result = try zigly.geo.lookup(allocator, client_ip, &geo_buf);
    const country = geo_result.value.country_code;

    // Block specific countries (in addition to ACL)
    const blocked_countries = [_][]const u8{ "XX", "YY" };
    for (blocked_countries) |blocked| {
        if (std.mem.eql(u8, country, blocked)) {
            try downstream.response.setStatus(403);
            try downstream.response.finish();
            return;
        }
    }

    try downstream.proxy("origin", null);
}

Local Testing Configuration

Configure ACLs in fastly.toml:

[local_server.acls]
  [local_server.acls.blocklist]
  file = "acls/blocklist.json"

  [local_server.acls.admin_ips]
  file = "acls/admin.json"

Create acls/blocklist.json:

{
  "entries": [
    {"prefix": "192.168.1.0/24", "action": "BLOCK"},
    {"prefix": "10.0.0.0/8", "action": "BLOCK"},
    {"prefix": "203.0.113.0/24", "action": "ALLOW"}
  ]
}

Create acls/admin.json:

{
  "entries": [
    {"prefix": "192.168.1.100/32", "action": "ALLOW"},
    {"prefix": "10.0.0.0/8", "action": "ALLOW"}
  ]
}

Test with different IPs:

curl -H "Fastly-Client-IP: 192.168.1.50" http://localhost:7878/
curl -H "Fastly-Client-IP: 8.8.8.8" http://localhost:7878/