Something went wrong. Try again.
atproto pds in zig pds.zat.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190//! Outbound fetches to URLs that arrive from callers, DID documents, or client//! metadata. Such a URL may only reach a public unicast address, and the//! connection is pinned to the address that was checked so a second DNS answer//! cannot move it. Redirects are refused because their targets are unchecked.
const std = @import("std");const zat = @import("zat");const config = @import("../core/config.zig");
pub const Error = error{ ForbiddenScheme, MissingHost, ForbiddenAddress, NoPublicAddress,};
pub const Target = struct { host: []const u8, dial_host: ?[]const u8,
pub fn resolvedConnection(self: Target) ?zat.HttpTransport.ResolvedConnection { return .{ .dial_host = self.dial_host orelse return null, .logical_host = self.host }; }};
/// Fetch `options.url` only if it reaches public unicast addresses.pub fn fetch( allocator: std.mem.Allocator, io: std.Io, transport: *zat.HttpTransport, options: zat.HttpTransport.FetchOptions,) !zat.HttpTransport.FetchResult { return transport.fetch(try guard(allocator, io, options));}
/// Pin `options` to a checked public address and refuse redirects.pub fn guard( allocator: std.mem.Allocator, io: std.Io, options: zat.HttpTransport.FetchOptions,) !zat.HttpTransport.FetchOptions { var guarded = options; guarded.on_redirect = .refuse; if (config.allowPrivateFetch()) return guarded; const target = try resolve(allocator, io, options.url); guarded.resolved_connection = target.resolvedConnection(); return guarded;}
/// Check a URL's host and choose the IPv4 address to dial. Every address the/// name resolves to must be public; one private answer rejects the name.pub fn resolve(allocator: std.mem.Allocator, io: std.Io, url: []const u8) !Target { const uri = std.Uri.parse(url) catch return error.MissingHost; if (!std.ascii.eqlIgnoreCase(uri.scheme, "https")) return error.ForbiddenScheme; const raw_host = switch (uri.host orelse return error.MissingHost) { .raw, .percent_encoded => |text| text, }; if (std.Io.net.IpAddress.parse(std.mem.trim(u8, raw_host, "[]"), 0)) |address| { if (!isPublic(address)) return error.ForbiddenAddress; return .{ .host = try allocator.dupe(u8, raw_host), .dial_host = null }; } else |_| {} var host_buf: [std.Io.net.HostName.max_len]u8 = undefined; const host_name = std.Io.net.HostName.fromUri(uri, &host_buf) catch return error.MissingHost; const host = try allocator.dupe(u8, host_name.bytes); const bare = std.mem.trimEnd(u8, host, "."); if (bare.len == 0 or std.ascii.eqlIgnoreCase(bare, "localhost") or std.ascii.endsWithIgnoreCase(bare, ".localhost")) { return error.ForbiddenAddress; }
var results: [32]std.Io.net.HostName.LookupResult = undefined; var queue: std.Io.Queue(std.Io.net.HostName.LookupResult) = .init(&results); host_name.lookup(io, &queue, .{ .port = 443 }) catch return error.NoPublicAddress; var dial: ?[4]u8 = null; var seen = false; while (queue.getOne(io)) |result| switch (result) { .address => |address| { seen = true; if (!isPublic(address)) return error.ForbiddenAddress; if (dial == null) switch (address) { .ip4 => |ip4| dial = ip4.bytes, .ip6 => {}, }; }, .canonical_name => {}, } else |err| switch (err) { error.Canceled => |e| return e, error.Closed => {}, } if (!seen) return error.NoPublicAddress; const bytes = dial orelse return error.NoPublicAddress; return .{ .host = host, .dial_host = try std.fmt.allocPrint(allocator, "{d}.{d}.{d}.{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3] }), };}
pub fn isPublic(address: std.Io.net.IpAddress) bool { return switch (address) { .ip4 => |ip4| isPublicIp4(ip4.bytes), .ip6 => |ip6| isPublicIp6(ip6.bytes), };}
const Ip4Range = struct { network: [4]u8, prefix: u6 };
const blocked_ip4 = [_]Ip4Range{ .{ .network = .{ 0, 0, 0, 0 }, .prefix = 8 }, .{ .network = .{ 10, 0, 0, 0 }, .prefix = 8 }, .{ .network = .{ 100, 64, 0, 0 }, .prefix = 10 }, .{ .network = .{ 127, 0, 0, 0 }, .prefix = 8 }, .{ .network = .{ 169, 254, 0, 0 }, .prefix = 16 }, .{ .network = .{ 172, 16, 0, 0 }, .prefix = 12 }, .{ .network = .{ 192, 0, 0, 0 }, .prefix = 24 }, .{ .network = .{ 192, 0, 2, 0 }, .prefix = 24 }, .{ .network = .{ 192, 88, 99, 0 }, .prefix = 24 }, .{ .network = .{ 192, 168, 0, 0 }, .prefix = 16 }, .{ .network = .{ 198, 18, 0, 0 }, .prefix = 15 }, .{ .network = .{ 198, 51, 100, 0 }, .prefix = 24 }, .{ .network = .{ 203, 0, 113, 0 }, .prefix = 24 }, .{ .network = .{ 224, 0, 0, 0 }, .prefix = 4 }, .{ .network = .{ 240, 0, 0, 0 }, .prefix = 4 },};
fn isPublicIp4(ip: [4]u8) bool { const candidate = std.mem.readInt(u32, &ip, .big); for (blocked_ip4) |range| { const network = std.mem.readInt(u32, &range.network, .big); const mask = ~@as(u32, 0) << @intCast(32 - range.prefix); if (candidate & mask == network & mask) return false; } return true;}
fn isPublicIp6(ip: [16]u8) bool { // IPv4-mapped (::ffff:0:0/96) and NAT64 (64:ff9b::/96) inherit the IPv4 policy. if (std.mem.allEqual(u8, ip[0..10], 0) and ip[10] == 0xff and ip[11] == 0xff) return isPublicIp4(ip[12..16].*); if (std.mem.eql(u8, ip[0..12], &[_]u8{ 0x00, 0x64, 0xff, 0x9b } ++ @as([8]u8, @splat(0)))) return isPublicIp4(ip[12..16].*); // Only global unicast (2000::/3), minus the special-purpose 2001::/23, // documentation 2001:db8::/32, and 6to4 2002::/16 blocks. if (ip[0] & 0xe0 != 0x20) return false; if (ip[0] == 0x20 and ip[1] == 0x01 and ip[2] & 0xfe == 0) return false; if (ip[0] == 0x20 and ip[1] == 0x01 and ip[2] == 0x0d and ip[3] == 0xb8) return false; if (ip[0] == 0x20 and ip[1] == 0x02) return false; return true;}
test "only public unicast addresses pass" { const cases = [_]struct { text: []const u8, public: bool }{ .{ .text = "8.8.8.8", .public = true }, .{ .text = "127.0.0.1", .public = false }, .{ .text = "10.1.2.3", .public = false }, .{ .text = "172.31.0.1", .public = false }, .{ .text = "172.32.0.1", .public = true }, .{ .text = "192.168.1.1", .public = false }, .{ .text = "169.254.169.254", .public = false }, .{ .text = "100.100.100.100", .public = false }, .{ .text = "0.0.0.0", .public = false }, .{ .text = "255.255.255.255", .public = false }, .{ .text = "2606:4700::1111", .public = true }, .{ .text = "::1", .public = false }, .{ .text = "fc00::1", .public = false }, .{ .text = "fdaa::3", .public = false }, .{ .text = "fe80::1", .public = false }, .{ .text = "::ffff:127.0.0.1", .public = false }, .{ .text = "::ffff:8.8.8.8", .public = true }, .{ .text = "64:ff9b::a00:1", .public = false }, .{ .text = "2001:db8::1", .public = false }, .{ .text = "2002:a00:1::", .public = false }, }; for (cases) |case| { const address = try std.Io.net.IpAddress.parse(case.text, 0); try std.testing.expectEqual(case.public, isPublic(address)); }}
test "literal and named private hosts are refused before any connection" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator(); const io = std.Options.debug_io; try std.testing.expectError(error.ForbiddenScheme, resolve(a, io, "http://example.com/x")); try std.testing.expectError(error.ForbiddenAddress, resolve(a, io, "https://127.0.0.1/x")); try std.testing.expectError(error.ForbiddenAddress, resolve(a, io, "https://[::1]:8443/x")); try std.testing.expectError(error.ForbiddenAddress, resolve(a, io, "https://169.254.169.254/latest/meta-data")); try std.testing.expectError(error.ForbiddenAddress, resolve(a, io, "https://localhost/x")); try std.testing.expectError(error.ForbiddenAddress, resolve(a, io, "https://app.localhost./x")); const public = try resolve(a, io, "https://8.8.8.8/x"); try std.testing.expect(public.dial_host == null);}