Plugging the Endpoint

With the firewall rules and sets in place, the final stage is to add the Phoenix module plug. You can add a plug to the router or a controller, but for this example we want to trap things early, so we add it to the endpoint.

I've added a simple Phoenix project to GitHub for demonstration purposes here and we'll work through it in this page. Phoenix offers such an elegant solution to enable the functionality we want that we only need to edit one file (lib/phx_firewallplug_web/endpoint.ex) and add our firewall module as a new file (lib/phx_firewallplug_web/plugs/firewall.ex).

For our simple firewall, we will create a module called PhxFirewallplugWeb.Plugs.Firewall and add a declaration in endpoint.ex before any other plugs, as below.

plug PhxFirewallplugWeb.Plugs.Firewall

Next, we create a new file where the plug code will go. The module must implement the init/1 and call/2 functions. In our case init/1 simply returns the connection options parameter. The real work is reserved for call/2, which examines the Plug.Conn connection struct to determine whether the request will be allowed through or not.

This simple firewall plug example will allow through HTTP GET and HEAD methods only. We also allow the request through if we cannot parse the conn.remote_ip value into a string representation of an IPv4 or IPv6 address.

with true <- conn.method not in [ "GET", "HEAD" ], { :ok, strIP, atomFamily } <- getIP(conn) do

As mentioned earlier, the IPv6 representation of an IPv4 address does not seem to match with ip6tables rules, so we must determine what type of IP address is passed. The getIP/1 function handles this by calling down to two private functions; getIPv6/1 and getIPv4/1, which call down to Erlang :inet functions to parse the string form of the conn.remote_ip property to determine IPv4 or IPv6 (or error).

If the with statement succeeds, we consider the IP address as hostile and blacklist it for one day, whereas if it fails, we simply return the Plug.Conn struct unchanged. The blacklist code checks the IP address familty returned by getIP/1 and makes a system command to ipset to add the IP address to the correct set. Note that the application will need to be running with root permissions for this to succeed.

if (atomFamily == :inet6), do: System.cmd("/usr/sbin/ipset",[ "add","blacklist_ipv6",strIP ]), else: System.cmd("/usr/sbin/ipset",[ "add","blacklist_ipv4",strIP ])

FĂ­nally, we send an HTTP 403 response and halt the plug pipeline, as no further processing of the request is required.

conn |> Plug.Conn.put_status(:forbidden) |> Plug.Conn.send_resp(403,"Forbidden") |> Plug.Conn.halt

I hope you found this information useful and stay tuned for more content to come.