Skip to content

Commit

Permalink
Merge pull-request #1
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-kearney committed Oct 12, 2023
2 parents e235493 + 4e6190f commit e71fca6
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 31 deletions.
57 changes: 57 additions & 0 deletions .github/workflows/publish-container.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: publish-container

on:
pull_request:
branches:
- main
push:
branches:
- main
tags:
- "v*.*.*"

jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Docker meta
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: |
ghcr.io/tkhq/traefik-plugin-geoblock
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=pr
type=semver,pattern={{version}}
type=sha,format=long
- name: Get committer date
run: |
echo "committer_date=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
platforms: linux/amd64
file: deploy/Dockerfile
build-args: |
SOURCE_DATE_EPOCH=${{ env.committer_date }}
provenance: "false"
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
7 changes: 5 additions & 2 deletions .traefik.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
displayName: geoblock
type: middleware
import: github.com/nscuro/traefik-plugin-geoblock
summary: traefik plugin to whitelist requests based on geolocation
summary: traefik plugin to block or allow requests based on geolocation
testData:
# It doesn't appear to be possible to get the pilot plugin analyzer
# to load local files. To prevent errors, the plugin is disabled here.
# This will cause the plugin to not attempt to load the database file.
enabled: false
# databaseFilePath: IP2LOCATION-LITE-DB1.IPV6.BIN
# allowedCountries: [ "CH", "DE" ]
# blockedCountries: [ "RU" ]
# defaultAllow: false
# allowPrivate: true
# disallowedStatusCode: 403
# allowedIPBlocks: ["66.249.64.0/19"]
# allowedIPBlocks: ["66.249.64.0/19"]
# blockedIPBlocks: ["66.249.64.0/24"]
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Latest GitHub release](https://img.shields.io/github/v/release/nscuro/traefik-plugin-geoblock?sort=semver)](https://github.com/nscuro/traefik-plugin-geoblock/releases/latest)
[![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](LICENSE)

*traefik-plugin-geoblock is a traefik plugin to whitelist requests based on geolocation*
*traefik-plugin-geoblock is a traefik plugin to allow or block requests based on geolocation*

> This projects includes IP2Location LITE data available from [`lite.ip2location.com`](https://lite.ip2location.com/database/ip-country).
Expand Down Expand Up @@ -49,10 +49,16 @@ http:
databaseFilePath: /plugins-local/src/github.com/nscuro/traefik-plugin-geoblock/IP2LOCATION-LITE-DB1.IPV6.BIN
# Whitelist of countries to allow (ISO 3166-1 alpha-2)
allowedCountries: [ "AT", "CH", "DE" ]
# Blocklist of countries to block (ISO 3166-1 alpha-2)
blockedCountries: [ "RU" ]
# Default allow indicates that if an IP is in neither block list nor allow lists, it should be allowed.
defaultAllow: false
# Allow requests from private / internal networks?
allowPrivate: true
# HTTP status code to return for disallowed requests (default: 403)
disallowedStatusCode: 204
# Add CIDR to be whitelisted, even if in a non-allowed country
allowedIPBlocks: ["66.249.64.0/19"]
```
# Add CIDR to be blacklisted, even if in an allowed country or IP block
blockedIPBlocks: ["66.249.64.5/32"]
```
17 changes: 17 additions & 0 deletions deploy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM busybox@sha256:023917ec6a886d0e8e15f28fb543515a5fcd8d938edb091e8147db4efed388ee

LABEL org.opencontainers.image.source https://github.com/tkhq/traefik-plugin-geoblock

RUN mkdir /dist

COPY go.mod /dist/
COPY go.sum /dist/
COPY .traefik.yml /dist/
COPY LICENSE /dist/
COPY plugin.go /dist/
COPY plugin.go /dist/
COPY vendor /dist/

COPY deploy/install.sh /

ENTRYPOINT ["/install.sh"]
7 changes: 7 additions & 0 deletions deploy/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh
set -e

# Copy this plugin to where Traefik is expecting it
mkdir -p /plugins-local/src/github.com/nscuro/traefik-plugin-geoblock
cp -R /dist/* /plugins-local/src/github.com/nscuro/traefik-plugin-geoblock/
cp /dist/.traefik.yml /plugins-local/src/github.com/nscuro/traefik-plugin-geoblock/
134 changes: 108 additions & 26 deletions plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ type Config struct {
Enabled bool // Enable this plugin?
DatabaseFilePath string // Path to ip2location database file
AllowedCountries []string // Whitelist of countries to allow (ISO 3166-1 alpha-2)
BlockedCountries []string // Blocklist of countries to be blocked (ISO 3166-1 alpha-2)
DefaultAllow bool // If source matches neither blocklist nor whitelist, should it be allowed through?
AllowPrivate bool // Allow requests from private / internal networks?
DisallowedStatusCode int // HTTP status code to return for disallowed requests
AllowedIPBlocks []string // List of whitelist CIDR
BlockedIPBlocks []string // List of blocklisted CIDRs
}

// CreateConfig creates the default plugin configuration.
Expand All @@ -36,16 +39,20 @@ type Plugin struct {
db *ip2location.DB
enabled bool
allowedCountries []string
blockedCountries []string
defaultAllow bool
allowPrivate bool
disallowedStatusCode int
allowedIPBlocks []*net.IPNet
blockedIPBlocks []*net.IPNet
}

// New creates a new plugin instance.
func New(_ context.Context, next http.Handler, cfg *Config, name string) (http.Handler, error) {
if next == nil {
return nil, fmt.Errorf("%s: no next handler provided", name)
}

if cfg == nil {
return nil, fmt.Errorf("%s: no config provided", name)
}
Expand Down Expand Up @@ -73,7 +80,12 @@ func New(_ context.Context, next http.Handler, cfg *Config, name string) (http.H
return nil, fmt.Errorf("%s: failed to open database: %w", name, err)
}

allowedIPBlocks, err := initAllowedIPBlocks(cfg.AllowedIPBlocks)
allowedIPBlocks, err := initIPBlocks(cfg.AllowedIPBlocks)
if err != nil {
return nil, fmt.Errorf("%s: failed loading allowed CIDR blocks: %w", name, err)
}

blockedIPBlocks, err := initIPBlocks(cfg.BlockedIPBlocks)
if err != nil {
return nil, fmt.Errorf("%s: failed loading allowed CIDR blocks: %w", name, err)
}
Expand All @@ -84,9 +96,12 @@ func New(_ context.Context, next http.Handler, cfg *Config, name string) (http.H
db: db,
enabled: cfg.Enabled,
allowedCountries: cfg.AllowedCountries,
blockedCountries: cfg.BlockedCountries,
defaultAllow: cfg.DefaultAllow,
allowPrivate: cfg.AllowPrivate,
disallowedStatusCode: cfg.DisallowedStatusCode,
allowedIPBlocks: allowedIPBlocks,
blockedIPBlocks: blockedIPBlocks,
}, nil
}

Expand Down Expand Up @@ -146,37 +161,92 @@ func (p Plugin) GetRemoteIPs(req *http.Request) []string {
}

// CheckAllowed checks whether a given IP address is allowed according to the configured allowed countries.
func (p Plugin) CheckAllowed(ip string) (bool, string, error) {
country, err := p.Lookup(ip)
func (p Plugin) CheckAllowed(ip string) (allow bool, country string, err error) {
var allowedCountry, allowedIP, blockedCountry, blockedIP bool
var allowedNetworkLength, blockedNetworkLength int

country, err = p.Lookup(ip)
if err != nil {
return false, "", fmt.Errorf("lookup of %s failed: %w", ip, err)
return false, ip, fmt.Errorf("lookup of %s failed: %w", ip, err)
}

if country == "-" { // Private address
if p.allowPrivate {
return true, ip, nil
if country == "-" {
return p.allowPrivate, country, nil
}

if country != "-" {
for _, item := range p.blockedCountries {
if item == country {
blockedCountry = true

break
}
}

return false, ip, nil
for _, item := range p.allowedCountries {
if item == country {
allowedCountry = true
}
}
}

blocked, blockedNetworkLength, err := p.isBlockedIPBlocks(ip)
if err != nil {
return false, ip, fmt.Errorf("failed to check if IP %q is blocked by IP block: %w", ip, err)
}

if blocked {
blockedIP = true
}

var allowed bool
for _, allowedCountry := range p.allowedCountries {
if allowedCountry == country {
return true, country, nil
return true, ip, nil
}
}

allowed, err = p.isAllowedIPBlocks(ip)
allowed, allowedNetBits, err := p.isAllowedIPBlocks(ip)
if err != nil {
return false, "", fmt.Errorf("checking if %s is part of an allowed range failed: %w", ip, err)
return false, ip, fmt.Errorf("failed to check if IP %q is allowed by IP block: %w", ip, err)
}

if !allowed {
if allowed {
allowedIP = true
allowedNetworkLength = allowedNetBits
}

// Handle final values
//
// NB: discrete IPs have higher priority than countries: more specific to less specific.

// NB: whichever matched prefix is longer has higher priority: more specific to less specific.
if allowedNetworkLength < blockedNetworkLength {
if blockedIP {
return false, country, nil
}

if allowedIP {
return true, country, nil
}
} else {
if allowedIP {
return true, country, nil
}

if blockedIP {
return false, country, nil
}
}

if allowedCountry {
return true, country, nil
}

if blockedCountry {
return false, country, nil
}

return true, country, nil
return p.defaultAllow, country, nil
}

// Lookup queries the ip2location database for a given IP address.
Expand All @@ -195,34 +265,46 @@ func (p Plugin) Lookup(ip string) (string, error) {
}

// Create IP Networks using CIDR block array
func initAllowedIPBlocks(allowedIPBlocks []string) ([]*net.IPNet, error) {
func initIPBlocks(ipBlocks []string) ([]*net.IPNet, error) {

var allowedIPBlocksNet []*net.IPNet
var ipBlocksNet []*net.IPNet

for _, cidr := range allowedIPBlocks {
for _, cidr := range ipBlocks {
_, block, err := net.ParseCIDR(cidr)
if err != nil {
return nil, fmt.Errorf("parse error on %q: %v", cidr, err)
}
allowedIPBlocksNet = append(allowedIPBlocksNet, block)
ipBlocksNet = append(ipBlocksNet, block)
}

return allowedIPBlocksNet, nil
return ipBlocksNet, nil
}

// isAllowedIPBlocks check if an IP is allowed base on the allowed CIDR blocks
func (p Plugin) isAllowedIPBlocks(ip string) (bool, error) {
var ipAddress net.IP = net.ParseIP(ip)
// isAllowedIPBlocks checks if an IP is allowed base on the allowed CIDR blocks
func (p Plugin) isAllowedIPBlocks(ip string) (bool, int, error) {
return p.isInIPBlocks(ip, p.allowedIPBlocks)
}

// isBlockedIPBlocks checks if an IP is allowed base on the blocked CIDR blocks
func (p Plugin) isBlockedIPBlocks(ip string) (bool, int, error) {
return p.isInIPBlocks(ip, p.blockedIPBlocks)
}

// isInIPBlocks indicates whether the given IP exists in any of the IP subnets contained within ipBlocks.
func (p Plugin) isInIPBlocks(ip string, ipBlocks []*net.IPNet) (bool, int, error) {
ipAddress := net.ParseIP(ip)

if ipAddress == nil {
return false, fmt.Errorf("unable parse IP address from address [%s]", ip)
return false, 0, fmt.Errorf("unable parse IP address from address [%s]", ip)
}

for _, block := range p.allowedIPBlocks {
for _, block := range ipBlocks {
if block.Contains(ipAddress) {
return true, nil
ones, _ := block.Mask.Size()

return true, ones, nil
}
}

return false, nil
return false, 0, nil
}
Loading

0 comments on commit e71fca6

Please sign in to comment.