Geofence your self-hosted web server and API's
Tags: go, webdev, security, tutorial
Published: 2021-11-27 19:45:00

## Background

Not long ago, [I made a post about my home-hosted interactive Halloween system](https://dev.to/circa10a/interactive-halloween-decorations-with-raspberry-pis-24el). It's basically a custom built REST API to interact with a bunch of smart things, but needed to be hosted at home to more easily interact with devices on my local network. I then exposed my system to the world, but the intent was to only have folks on my street be able to interact with my Halloween decorations. 
It's no fun when folks from across my neighborhood hear about it, go to the site and trigger a bunch effects, and no one be there to enjoy. The other scenario is that I have friends on the other side of the country mess with me by having my lights flash red and blue 🚨 on repeat (looking at you Traci).

## Geofencing

What did I do to solve this? I implemented something called IP geofencing on my self-hosted API.

So what is geofencing?

IP geofencing is a security measure that restricts IP availability by geography, regardless of a user's access permissions. Think of it as a virtual perimeter around a given location.

### But how?

I wrote a library called [go-geofence](https://github.com/circa10a/go-geofence) that uses [ipbase.com](https://ipbase.com/) behind these scenes because the service gives you 150 requests per month for free and 65,000 per month for $10.

## How does it work?

I wanted something free and simple. Here's some example usage of the Go library I wrote below. All you need is an API key from [ipbase.com](https://ipbase.com/).

When the client is instantiated, it uses [ipbase.com](https://ipbase.com/) to determine the GPS coordinates of the client running the software. Then when the `IsIPAddressNear()` method is called, the library then looks up the GPS coordinates of the IP address specified, then compares the coordinates and computes distance in kilometers and determines if the IP address' location is close to yours.

```go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/circa10a/go-geofence"
)

func main() {
	geofence, err := geofence.New(&geofence.Config{
		// Empty string to geofence your current public IP address, or you can monitor a remote address by supplying it as the first parameter
		IPAddress: "",
		// ipbase.com API token
		Token: "YOUR_IPBASE_API_TOKEN",
		// Maximum radius of the geofence in kilometers, only clients less than or equal to this distance will return true with isAddressNearby
		// 1 kilometer
		Radius: 1.0,
		CacheTTL:    7 * (24 * time.Hour), // 1 week
	})
	if err != nil {
		log.Fatal(err)
	}
	isAddressNearby, err := geofence.IsIPAddressNear("8.8.8.8")
	if err != nil {
		log.Fatal(err)
	}
	// Address nearby: false
	fmt.Println("Address nearby: ", isAddressNearby)
}
```

## How was it implemented?

Since I'm using [echo](https://echo.labstack.com/) as the web framework to control my decorations, I was able to implement some [pretty simple middleware](https://github.com/circa10a/witchonstephendrive.com/blob/main/routes/middleware/geofencing/geofencing.go) that rejects `POST` requests from IP addresses that aren't within close proximity to mine with a `403` status code.

![alt text](https://media.giphy.com/media/xUNd9RCjr18e5lfXqg/giphy.gif)

## I want to implement the same thing for my home web server/API, but don't use Go

Well, boy do I have the solution for you!

I was surprised with the lack of open source geofencing solutions for this when I was originally looking into it. So to make the implementation more accessible, I built a module for the open source web server called [Caddy](https://caddyserver.com/).

## Caddy

[Caddy](https://caddyserver.com/) is an **awesome** web server alternative to [nginx](https://www.nginx.com/) and [apache (httpd)](https://httpd.apache.org/). Caddy is written in Go, is a much more performant and extensible web server (in my opinion). With Caddy, you can host basic files or reverse proxy your API's, which is how I use it. I use Caddy because of its automatic TLS functionality so I don't have to worry about manual creation of certificates and keys.

## caddy-geofence

To integrate with Caddy, I wrote a [geofencing module](https://github.com/circa10a/caddy-geofence) than can be used with a wealth of configuration options thanks to Caddy's versatile abilities. You can configure the module per route, request method, pretty much any condition you can think of.

To implement the same geofencing functionality that I did to restrict who can use your HTTP services, all you need is an API key from [ipbase.com](https://ipbase.com/) and create a new web server with a [Caddyfile](https://caddyserver.com/docs/caddyfile) to specify your token and configuration preferences.

If you're new to Caddy, one thing to note about Caddy and installing modules is that modules are compiled with Caddy, thus resulting in a single binary to execute.

Here's an example Caddyfile you would use to configure Caddy with geofencing functionality:

```
{
	order geofence before respond
}

:80

route /* {
	geofence {
		# cache_ttl is the duration to store ip addresses and if they are within proximity or not to increase performance
		# Cache for 7 days, valid time units are "ms", "s", "m", "h"
		# Not specifying a TTL sets no expiration on cached items and will live until restart
		cache_ttl 168h

		# ipbase.com API token, this example reads from an environment variable
		ipbase_api_token {$IPBASE_API_TOKEN}

		# radius is the the distance of the geofence, only clients within the distance will be allowed.
		# If not supplied, will default to 0.0 kilometers
		radius 1.0

		# allow_private_ip_addresses is a boolean for whether or not to allow private ip ranges
		# such as 192.X, 172.X, 10.X, [::1] (localhost)
		# false by default
		# Some cellular networks doing NATing with 172.X addresses, in which case, you may not want to allow
		allow_private_ip_addresses true

		# allowlist is a list of IP addresses that will not be checked for proximity and will be allowed to access the server
		allowlist 206.189.205.251 206.189.205.252

		# status_code is the HTTP response code that is returned if IP address is not within proximity. Default is 403
		status_code 403
	}
}

log {
	output stdout
}
```

Then to run Caddy with the module installed:

```shell
docker run --net host -v /your/Caddyfile:/etc/caddy/Caddyfile -e IPBASE_API_TOKEN -p 80:80 -p 443:443 circa10a/caddy-geofence
```

Or if if you would rather compile Caddy yourself with the module and not use docker, you can use [xcaddy](https://caddyserver.com/docs/build#xcaddy) like so:

```shell
xcaddy build --with github.com/circa10a/caddy-geofence
```

For more info on implementing a geofenced web server/reverse proxy with caddy, see the [caddy-geofence repo](https://github.com/circa10a/caddy-geofence)

## Fin

And there you have it, expose your web server or API's and limit who can use them based on proximity to your physical location.