First version
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.idea
|
||||||
|
.env
|
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY src/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY src/netflix-asn.py .
|
||||||
|
COPY src/crontab.txt .
|
||||||
|
|
||||||
|
RUN chmod 0644 crontab.txt \
|
||||||
|
&& apt-get update && apt-get install -y cron \
|
||||||
|
&& crontab crontab.txt
|
||||||
|
|
||||||
|
CMD printenv > /etc/environment && cron -f
|
104
README.md
Normal file
104
README.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# netflix-asn
|
||||||
|
|
||||||
|
A small **Python utility** that fetches IPv4/IPv6 prefixes announced by one or more ASNs (via the [BGPView API](https://bgpview.io/api)) and ensures those prefixes are present in a MikroTik **IP firewall address-list**.
|
||||||
|
|
||||||
|
It’s designed to run inside **Docker** — using a `Dockerfile` and `docker-compose.yml`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fetches all IPv4/IPv6 prefixes announced by one or more ASNs.
|
||||||
|
- Adds missing prefixes to a MikroTik address-list.
|
||||||
|
- Skips existing entries to avoid duplicates.
|
||||||
|
- Logs progress and errors clearly.
|
||||||
|
- Suitable for manual or scheduled execution.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Create a `.env` file (see [Example .env](#example-env)).
|
||||||
|
2. Build the Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. View logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f asn-syncer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Target ASN(s) — default is AS2906 (Netflix)
|
||||||
|
ASN=AS55095,AS40027,AS394406,AS2906
|
||||||
|
|
||||||
|
# MikroTik API connection
|
||||||
|
MIKROTIK_HOST=192.168.88.1
|
||||||
|
USERNAME=admin
|
||||||
|
PASSWORD=verysecret
|
||||||
|
|
||||||
|
# Name of the address-list on the MikroTik
|
||||||
|
ADDRESS_LIST_NAME=Netflix
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Tip:** Keep your `.env` file out of version control.
|
||||||
|
> Use Docker secrets or a secure secrets manager for production deployments.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|---------------------|----------|-----------|------------------------------------------------------|
|
||||||
|
| `ASN` | No | `AS2906` | Comma-separated list of ASNs to fetch prefixes from. |
|
||||||
|
| `MIKROTIK_HOST` | Yes | — | IP or hostname of the MikroTik device. |
|
||||||
|
| `USERNAME` | Yes | — | MikroTik API username. |
|
||||||
|
| `PASSWORD` | Yes | — | MikroTik API password. |
|
||||||
|
| `ADDRESS_LIST_NAME` | No | `Netflix` | MikroTik address-list name to add entries to. |
|
||||||
|
|
||||||
|
> The script sets a fixed `timeout=24:00:00` for each address-list entry.
|
||||||
|
> Modify the script if you prefer permanent entries.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. The script loads configuration from environment variables.
|
||||||
|
2. For each ASN, it queries:
|
||||||
|
```
|
||||||
|
https://api.bgpview.io/asn/<ASN>/prefixes
|
||||||
|
```
|
||||||
|
3. It collects all IPv4/IPv6 prefixes and removes duplicates.
|
||||||
|
4. Connects to the MikroTik API using [`librouteros`](https://pypi.org/project/librouteros/).
|
||||||
|
5. For each prefix:
|
||||||
|
- Skips it if it already exists in the address-list.
|
||||||
|
- Otherwise adds it with:
|
||||||
|
- `timeout=24:00:00`
|
||||||
|
- `comment="Added from ASN"`
|
||||||
|
|
||||||
|
## Logging & Exit Codes
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|-----------------|------------------------------------------------------------|
|
||||||
|
| **INFO** | Normal progress messages (connection, added subnets, etc). |
|
||||||
|
| **DEBUG** | Skipped subnets that already exist. |
|
||||||
|
| **ERROR/FATAL** | Connection or API failure. |
|
||||||
|
|
||||||
|
| Exit Code | Meaning |
|
||||||
|
|------------|--------------------------------------------------------------|
|
||||||
|
| `0` | Success |
|
||||||
|
| `1` | Fatal error (missing vars, API failure, or connection error) |
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Never commit credentials or `.env` files to Git.
|
||||||
|
- Use dedicated API accounts on MikroTik with minimal permissions.
|
||||||
|
- Run the container within a trusted network or over a secure VPN.
|
||||||
|
- Use `Docker secrets` for sensitive information in production.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is provided under the [MIT License](LICENSE) — free for personal and commercial use.
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
netflix-asn:
|
||||||
|
build: .
|
||||||
|
container_name: netflix-asn
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./logs:/var/log
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: {}
|
2
src/crontab.txt
Normal file
2
src/crontab.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
SHELL=/bin/bash
|
||||||
|
0 * * * * /usr/local/bin/python3 /app/netflix-asn.py >> /var/log/cron.log 2>&1
|
111
src/netflix-asn.py
Normal file
111
src/netflix-asn.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from typing import List, Set
|
||||||
|
from librouteros import connect
|
||||||
|
from librouteros.query import Key
|
||||||
|
from librouteros.exceptions import TrapError
|
||||||
|
|
||||||
|
# Constants and config
|
||||||
|
DEFAULT_ASN = "AS2906" # Netflix ASN
|
||||||
|
TIMEOUT = "24:00:00"
|
||||||
|
|
||||||
|
ASN = os.getenv("ASN", DEFAULT_ASN).split(',')
|
||||||
|
MIKROTIK_HOST = os.getenv("MIKROTIK_HOST")
|
||||||
|
USERNAME = os.getenv("USERNAME")
|
||||||
|
PASSWORD = os.getenv("PASSWORD")
|
||||||
|
ADDRESS_LIST_NAME = os.getenv("ADDRESS_LIST_NAME", "Netflix")
|
||||||
|
|
||||||
|
def get_subnets_from_asn(asn: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Queries BGPView API to get all IPv4 prefixes announced by an ASN.
|
||||||
|
"""
|
||||||
|
logging.info(f"Fetching prefixes for ASN {asn}...")
|
||||||
|
url = f"https://api.bgpview.io/asn/{asn}/prefixes"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise RuntimeError(f"Failed to fetch ASN data: {e}") from e
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
prefixes = [item['prefix'] for item in data['data']['ipv4_prefixes']]
|
||||||
|
logging.info(f"Found {len(prefixes)} IPv4 prefixes for ASN {asn}.")
|
||||||
|
return prefixes
|
||||||
|
|
||||||
|
def get_subnets_from_asns(asns: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Fetch prefixes from multiple ASNs.
|
||||||
|
"""
|
||||||
|
subnets: Set[str] = set()
|
||||||
|
for asn in asns:
|
||||||
|
subnets.update(get_subnets_from_asn(asn))
|
||||||
|
time.sleep(2) # Respect API rate limits
|
||||||
|
return list(subnets)
|
||||||
|
|
||||||
|
def subnet_exists(address_list, subnet: str, list_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a subnet already exists in the MikroTik address list.
|
||||||
|
"""
|
||||||
|
entries = address_list.select().where(Key('address') == subnet, Key('list') == list_name)
|
||||||
|
return any(entries)
|
||||||
|
|
||||||
|
def add_subnet_address_list(address_list, subnet: str, list_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Add a single subnet to the MikroTik address list.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
address_list.add(address=subnet, list=list_name, timeout=TIMEOUT, comment="Added from ASN")
|
||||||
|
return True
|
||||||
|
except TrapError as err:
|
||||||
|
logging.error(f"Failed to add subnet {subnet}: {err}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_subnets_address_list(address_list, subnets: List[str], list_name: str) -> int:
|
||||||
|
"""
|
||||||
|
Add multiple subnets to the MikroTik address list.
|
||||||
|
"""
|
||||||
|
added = 0
|
||||||
|
for subnet in subnets:
|
||||||
|
if subnet_exists(address_list, subnet, list_name):
|
||||||
|
logging.debug(f"[SKIP] {subnet} already exists.")
|
||||||
|
continue
|
||||||
|
if add_subnet_address_list(address_list, subnet, list_name):
|
||||||
|
logging.info(f"[ADD] {subnet}")
|
||||||
|
added += 1
|
||||||
|
return added
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not MIKROTIK_HOST or not USERNAME or not PASSWORD:
|
||||||
|
logging.fatal("Missing MikroTik connection info (check env vars)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
logging.info(f"Connecting to MikroTik at {MIKROTIK_HOST}...")
|
||||||
|
try:
|
||||||
|
api = connect(host=MIKROTIK_HOST, username=USERNAME, password=PASSWORD)
|
||||||
|
except Exception as e:
|
||||||
|
logging.fatal(f"Failed to connect to MikroTik: {e}", exc_info=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
address_list = api.path('ip', 'firewall', 'address-list')
|
||||||
|
|
||||||
|
try:
|
||||||
|
subnets = get_subnets_from_asns(ASN)
|
||||||
|
except Exception as e:
|
||||||
|
logging.fatal(f"Failed to get subnets from ASN: {e}", exc_info=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
added = add_subnets_address_list(address_list, subnets, ADDRESS_LIST_NAME)
|
||||||
|
logging.info(f"Done. Added {added} new subnets.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
2
src/requirements.txt
Normal file
2
src/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
requests
|
||||||
|
librouteros
|
Reference in New Issue
Block a user