DEV Community

Cover image for Geofence your self-hosted web server and API's
Caleb Lemoine
Caleb Lemoine

Posted on • Updated on

Geofence your self-hosted web server and API's

Background

Not long ago, I made a post about my home-hosted interactive Halloween system. 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 that uses freegeoip.app behind these scenes (because the service gives you 15,000 request per hour for free).

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 freegeoip.app.

When the client is instantiated, it uses freegeoip.app 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 down to the specified decimal point (the library's usage of sensitivity) and determines if the IP address' location is close to yours.

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: "",
        // freegeoip.app API token
        Token: "YOUR_FREEGEOIP_API_TOKEN",
        // Sensitivity
        // 0 - 111 km
        // 1 - 11.1 km
        // 2 - 1.11 km
        // 3 111 meters
        // 4 11.1 meters
        // 5 1.11 meters
        Sensitivity: 3,                    // 3 is recommended
        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)
}
Enter fullscreen mode Exit fullscreen mode

How was it implemented?

Since I'm using echo as the web framework to control my decorations, I was able to implement some pretty simple middleware that rejects POST requests from IP addresses that aren't within close proximity to mine with a 403 status code.

alt text

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.

Caddy

Caddy is an awesome web server alternative to nginx and apache (httpd). 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 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 freegeoip.app and create a new web server with a 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

        # freegeoip.app API token, this example reads from an environment variable
        freegeoip_api_token {$FREEGEOIP_API_TOKEN}

        # sensitivity (proximity)
        # 0 - 111 km
        # 1 - 11.1 km
        # 2 - 1.11 km
        # 3 111 meters
        # 4 11.1 meters
        # 5 1.11 meters
        sensitivity 3

        # 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
}
Enter fullscreen mode Exit fullscreen mode

Then to run Caddy with the module installed:

docker run --net host -v /your/Caddyfile:/etc/caddy/Caddyfile -e FREEGEOIP_API_TOKEN -p 80:80 -p 443:443 circa10a/caddy-geofence
Enter fullscreen mode Exit fullscreen mode

Or if if you would rather compile Caddy yourself with the module and not use docker, you can use xcaddy like so:

xcaddy build --with github.com/circa10a/caddy-geofence
Enter fullscreen mode Exit fullscreen mode

For more info on implementing a geofenced web server/reverse proxy with caddy, see the caddy-geofence repo

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.

Discussion (0)