Skip to content
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
5 changes: 5 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ clampip
pseudoprofound
reimagining
iocaine
admins
fout
iplist
NArg
blocklists
1 change: 1 addition & 0 deletions docs/docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Add iplist2rule tool that lets admins turn an IP address blocklist into an Anubis ruleset.
Comment thread Fixed
Comment thread Fixed
- Add Polish locale ([#1292](https://github.com/TecharoHQ/anubis/pull/1309))

<!-- This changes the project to: -->
Expand Down
50 changes: 50 additions & 0 deletions docs/docs/admin/iplist2rule.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: iplist2rule CLI tool
---

The `iplist2rule` tool converts IP blocklists into Anubis challenge policies. It reads common IP block list formats and generates the appropriate Anubis policy file for IP address filtering.
Comment thread Fixed

## Installation

Install directly with Go

```bash
go install github.com/TecharoHQ/anubis/utils/cmd/iplist2rule@latest
```

## Usage

Basic conversion from URL:

```bash
iplist2rule https://raw.githubusercontent.com/7c/torfilter/refs/heads/main/lists/txt/torfilter-1m-flat.txt filter-tor.yaml
```

Explicitly allow every IP address on a list:

```bash
iplist2rule --action ALLOW https://raw.githubusercontent.com/7c/torfilter/refs/heads/main/lists/txt/torfilter-1m-flat.txt filter-tor.yaml
```

Add weight to requests matching IP addresses on a list:

```bash
iplist2rule --action WEIGH --weight 20 https://raw.githubusercontent.com/7c/torfilter/refs/heads/main/lists/txt/torfilter-1m-flat.txt filter-tor.yaml
```

## Options

| Flag | Description | Default |
| :------------ | :----------------------------------------------------------------------------------------------- | :-------------------------------- |
| `--action` | The Anubis action to take for the IP address in question, must be in ALL CAPS. | `DENY` (forbids traffic) |
| `--rule-name` | The name for the generated Anubis rule, should be in kebab-case. | (not set, inferred from filename) |
| `--weight` | When `--action=WEIGH`, how many weight points should be added or removed from matching requests? | 0 (not set) |

## Using the Generated Policy

Save the output and import it in your main policy file:

```yaml
bots:
- import: "./filter-tor.yaml"
```
11 changes: 7 additions & 4 deletions docs/docs/admin/robots2policy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Install directly with Go:
```bash
go install github.com/TecharoHQ/anubis/cmd/robots2policy@latest
```

## Usage

Basic conversion from URL:
Expand All @@ -35,8 +36,8 @@ robots2policy -input robots.txt -action DENY -format json
## Options

| Flag | Description | Default |
|-----------------------|--------------------------------------------------------------------|---------------------|
| `-input` | robots.txt file path or URL (use `-` for stdin) | *required* |
| --------------------- | ------------------------------------------------------------------ | ------------------- |
| `-input` | robots.txt file path or URL (use `-` for stdin) | _required_ |
| `-output` | Output file (use `-` for stdout) | stdout |
| `-format` | Output format: `yaml` or `json` | `yaml` |
| `-action` | Action for disallowed paths: `ALLOW`, `DENY`, `CHALLENGE`, `WEIGH` | `CHALLENGE` |
Expand All @@ -47,6 +48,7 @@ robots2policy -input robots.txt -action DENY -format json
## Example

Input robots.txt:

```txt
User-agent: *
Disallow: /admin/
Expand All @@ -57,6 +59,7 @@ Disallow: /
```

Generated policy:

```yaml
- name: robots-txt-policy-disallow-1
action: CHALLENGE
Expand All @@ -77,8 +80,8 @@ Generated policy:
Save the output and import it in your main policy file:

```yaml
import:
- path: "./robots-policy.yaml"
bots:
- import: "./robots-policy.yaml"
```

The tool handles wildcard patterns, user-agent specific rules, and blacklisted bots automatically.
57 changes: 57 additions & 0 deletions utils/cmd/iplist2rule/blocklist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main
Comment thread Fixed

import (
"bufio"
"fmt"
"io"
"net/http"
"net/netip"
"strings"
)

// FetchBlocklist reads the blocklist over HTTP and returns every non-commented
// line parsed as an IP address in CIDR notation. IPv4 addresses are returned as
// /32, IPv6 addresses as /128.
//
// This function was generated with GLM 4.7.
func FetchBlocklist(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP request failed with status: %s", resp.Status)
}

var lines []string
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
// Skip empty lines and comments (lines starting with #)
if line == "" || strings.HasPrefix(line, "#") {
continue
}

addr, err := netip.ParseAddr(line)
if err != nil {
// Skip lines that aren't valid IP addresses
continue
}

var cidr string
if addr.Is4() {
cidr = fmt.Sprintf("%s/32", addr.String())
} else {
cidr = fmt.Sprintf("%s/128", addr.String())
}
lines = append(lines, cidr)
}

if err := scanner.Err(); err != nil && err != io.EOF {
return nil, err
}

return lines, nil
}
103 changes: 103 additions & 0 deletions utils/cmd/iplist2rule/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main
Comment thread Fixed

import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"

"github.com/TecharoHQ/anubis/lib/config"
"github.com/facebookgo/flagenv"
"sigs.k8s.io/yaml"
)

type Rule struct {
Name string `yaml:"name" json:"name"`
Action config.Rule `yaml:"action" json:"action"`
RemoteAddr []string `json:"remote_addresses,omitempty" yaml:"remote_addresses,omitempty"`
Weight *config.Weight `json:"weight,omitempty" yaml:"weight,omitempty"`
}

func init() {
flag.Usage = func() {
fmt.Printf(`Usage of %[1]s:

%[1]s [flags] <blocklist-url> <filename>

Grabs the contents of the blocklist, converts it to an Anubis ruleset, and writes it to filename.

Flags:
`, filepath.Base(os.Args[0]))

flag.PrintDefaults()
}
}

var (
action = flag.String("action", "DENY", "Anubis action to take (ALLOW / DENY / WEIGH)")
manualRuleName = flag.String("rule-name", "", "If set, prefer this name over inferring from filename")
weight = flag.Int("weight", 0, "If set to any number, add/subtract this many weight points when --action=WEIGH")
)

func main() {
flagenv.Parse()
flag.Parse()

if flag.NArg() != 2 {
Comment thread Fixed
flag.Usage()
os.Exit(2)
}

blocklistURL := flag.Arg(0)
foutName := flag.Arg(1)
Comment thread Fixed
ruleName := strings.TrimSuffix(foutName, filepath.Ext(foutName))
Comment thread Fixed
Comment thread Fixed

if *manualRuleName != "" {
ruleName = *manualRuleName
}

ruleAction := config.Rule(*action)
if err := ruleAction.Valid(); err != nil {
log.Fatalf("--action=%q is invalid: %v", *action, err)
}

result := &Rule{
Name: ruleName,
Action: ruleAction,
}

if *weight != 0 {
if ruleAction != config.RuleWeigh {
log.Fatalf("used --weight=%d but --action=%s", *weight, *action)
}

result.Weight = &config.Weight{
Adjust: *weight,
}
}

ips, err := FetchBlocklist(blocklistURL)
if err != nil {
log.Fatalf("can't fetch blocklist %s: %v", blocklistURL, err)
}

result.RemoteAddr = ips

fout, err := os.Create(foutName)
Comment thread Fixed
Comment thread Fixed
if err != nil {
log.Fatalf("can't create output file %q: %v", foutName, err)
}
defer fout.Close()

fmt.Fprintf(fout, "# Generated by %s on %s from %s\n\n", filepath.Base(os.Args[0]), time.Now().Format(time.RFC3339), blocklistURL)

data, err := yaml.Marshal([]*Rule{result})
if err != nil {
log.Fatalf("can't marshal yaml")
}

fout.Write(data)
}
Loading