Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add blacklist #1

Merged
merged 4 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading