Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ clampip
pseudoprofound
reimagining
iocaine
admins
fout
iplist
NArg
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