Skip to main content

src/masque_ip.erl

%%% @doc IP address classification for SSRF protection.
%%%
%%% `is_public/1' returns `false' for loopback, RFC 1918 private,
%%% link-local, multicast, and reserved address ranges in both
%%% IPv4 and IPv6. The built-in proxy handlers call this after DNS
%%% resolution to reject tunnels targeting internal networks.
-module(masque_ip).

-export([is_public/1, reject_requests/1, inject_packet/2]).

-include("masque_ip.hrl").

%% @doc Build the RFC 9484 ยง5.2 "reject all" answer to a batch of
%% inbound ADDRESS_REQUEST entries: each reply carries the same
%% Request ID and IP Version, an all-zero address, and the maximum
%% prefix length for that version (32 for v4, 128 for v6).
-spec reject_requests([#ip_prefix_request{}]) -> [#ip_assignment{}].
reject_requests(Requests) ->
    [reject_one(R) || R <- Requests].

reject_one(#ip_prefix_request{request_id = Id, version = 4}) ->
    #ip_assignment{request_id = Id, version = 4,
                   address = {0,0,0,0}, prefix_len = 32};
reject_one(#ip_prefix_request{request_id = Id, version = 6}) ->
    #ip_assignment{request_id = Id, version = 6,
                   address = {0,0,0,0,0,0,0,0}, prefix_len = 128}.

%% @doc Push an IP packet into a server session for delivery to its
%% connected client. Non-blocking. Intended for out-of-band injectors
%% (e.g. a TUN device owner that holds the session pid via the
%% address registry); accepted by both `masque_ip_server_session'
%% (h2/h3) and `masque_ip_h1_server_session' (h1).
-spec inject_packet(pid(), binary()) -> ok.
inject_packet(SessionPid, Packet) when is_pid(SessionPid),
                                       is_binary(Packet) ->
    gen_server:cast(SessionPid, {inject_packet, Packet}).

-spec is_public(inet:ip_address()) -> boolean().

%% IPv4
is_public({0,_,_,_})                                      -> false; %% 0.0.0.0/8
is_public({10,_,_,_})                                     -> false; %% RFC 1918
is_public({100,B,_,_}) when B >= 64, B =< 127             -> false; %% RFC 6598 CGN
is_public({127,_,_,_})                                    -> false; %% loopback
is_public({169,254,_,_})                                  -> false; %% link-local
is_public({172,B,_,_}) when B >= 16, B =< 31              -> false; %% RFC 1918
is_public({192,0,0,_})                                    -> false; %% RFC 6890
is_public({192,0,2,_})                                    -> false; %% TEST-NET-1
is_public({192,88,99,_})                                  -> false; %% 6to4 relay
is_public({192,168,_,_})                                  -> false; %% RFC 1918
is_public({198,18,_,_})                                   -> false; %% benchmarking
is_public({198,19,_,_})                                   -> false; %% benchmarking
is_public({198,51,100,_})                                 -> false; %% TEST-NET-2
is_public({203,0,113,_})                                  -> false; %% TEST-NET-3
is_public({A,_,_,_}) when A >= 224                        -> false; %% multicast + reserved
%% IPv6
is_public({0,0,0,0,0,0,0,0})                             -> false; %% ::  unspecified
is_public({0,0,0,0,0,0,0,1})                             -> false; %% ::1 loopback
is_public({0,0,0,0,0,16#FFFF,_,_})                       -> false; %% ::ffff:0:0/96 mapped v4
is_public({16#64,16#FF9B,_,_,_,_,_,_})                   -> false; %% 64:ff9b::/96 NAT64
is_public({16#100,_,_,_,_,_,_,_})                        -> false; %% discard 100::/64
is_public({16#2001,0,_,_,_,_,_,_})                       -> false; %% teredo
is_public({16#2001,16#DB8,_,_,_,_,_,_})                  -> false; %% documentation
is_public({A,_,_,_,_,_,_,_}) when A >= 16#FC00,
                                   A =< 16#FDFF          -> false; %% ULA fc00::/7
is_public({A,_,_,_,_,_,_,_}) when A >= 16#FE80,
                                   A =< 16#FEBF          -> false; %% link-local fe80::/10
is_public({A,_,_,_,_,_,_,_}) when A >= 16#FF00           -> false; %% multicast ff00::/8
is_public(_)                                              -> true.