# Routers

## Project Overview

The **Routers** project is a Python-based tool designed to manage firewall aliases across different routers,
specifically **pfSense** (v1 and v2) and **OPNsense**. It provides a unified interface for creating, updating, deleting,
and renaming firewall aliases, while abstracting away the differences between the APIs of these platforms.

This tool leverages client libraries for pfSense and OPNsense APIs, allowing seamless interaction with their
respective firewall alias management functionalities. The core of the project is the `Router` class which handles
connection to different router types, and the alias adapters which convert between internal data structures and
router-specific models.

## Features

- **Unified Router Management:** Connect to pfSense v1, pfSense v2, and OPNsense routers using a common interface.
- **Advanced Alias Operations:** Create, update, delete, and rename firewall aliases without needing to know the details of the underlying API.
- **Secure Configuration Storage:** Store router connection details securely using Redis with encryption.
- **Adapter Architecture:** Flexible adapter system to handle different router types and API versions.
- **Difference Detection:** Identify differences between aliases across routers to help with synchronization.
- **Comprehensive Testing:** Includes a robust test suite with mock objects to ensure code correctness without needing live routers.

## Installation

### Prerequisites

- Python 3.14 or higher
- Core dependencies:
  - `pyfsense-client` - Client library for pfSense
  - `opn-api` - Client library for OPNsense
  - `redis[hiredis]` - For configuration storage
  - `cryptography` - For secure configuration storage
  - Additional dependencies specified in pyproject.toml

### Steps

1. Clone this repository:

   ```bash
   git clone https://github.com/devinbarry/routers.git
   cd routers
   ```

2. Install dependencies:

   ```bash
   uv sync
   ```

## Project Structure

```
routers/
│
├── src/routers/
│   ├── __init__.py
│   ├── settings.py                    # Environment configuration
│   ├── firewall.py                    # Firewall utilities
│   ├── alias/                         # Alias management
│   │   ├── types.py                   # Alias dataclass, AliasType enum
│   │   ├── operations.py              # High-level sync operations
│   │   ├── loader.py                  # JSON alias loading & reference lookup
│   │   ├── adapter_factory.py         # Creates adapter for a given router
│   │   ├── type_conversions.py        # Router-specific format conversions
│   │   ├── exceptions.py              # Alias exceptions
│   │   └── adapter/                   # Platform-specific adapters
│   │       ├── base.py                # Abstract adapter interface
│   │       ├── pfsense_v1.py          # pfSense v1 adapter
│   │       ├── pfsense_v2.py          # pfSense v2 adapter
│   │       └── opnsense.py            # OPNsense adapter
│   ├── client/
│   │   └── pfsense_v2.py              # PfSenseV2MetaClient wrapper
│   └── router/
│       ├── core.py                    # Router dataclass, RouterType, ClientFactory
│       ├── exceptions.py              # Router exceptions
│       └── config/
│           ├── encrypt.py             # Fernet-encrypted Redis wrapper
│           └── store.py               # ConfigLoader & CredentialStore
│
├── tests/                             # Test suite organized by module
├── pyproject.toml
└── README.md
```

## Usage

### Initialize a Router

```python
from routers import Router, RouterType

router = Router(name="MyRouter", type=RouterType.PFSENSE_V2)
```

Router credentials are loaded from encrypted Redis via `ConfigLoader`. See `router/config/` for setup.

### Direct Alias Operations (Adapter)

Use the adapter for direct CRUD on individual aliases:

```python
from routers.alias.adapter_factory import get_alias_adapter
from routers.alias.types import Alias, AliasType

adapter = get_alias_adapter(router)

# List all aliases
aliases = adapter.get_all_aliases()

# Create
alias = Alias(
    name="BlockList",
    alias_type=AliasType.HOST,
    description="Block list of IPs",
    content=["192.168.1.1", "192.168.1.2"],
    detail=["IP1", "IP2"],
)
adapter.create_alias(alias)

# Update
alias.description = "Updated block list"
adapter.update_alias(alias)

# Delete
adapter.delete_alias("BlockList")

# Rename
adapter.rename_alias(alias, "NewBlockList")
```

### Sync Operations (Reference-Based)

The `operations` module syncs router aliases against reference definitions:

```python
from routers.alias.operations import (
    print_differing_client_aliases,
    update_differences_on_client,
    create_new_on_client,
    delete_missing_reference_aliases,
)

# Show differences between router state and reference aliases
print_differing_client_aliases(router)

# Sync: update differing aliases, create missing ones
update_differences_on_client(router)

# Create only missing aliases (no updates)
create_new_on_client(router)

# Delete aliases not in the reference list
delete_missing_reference_aliases(router)
```

### Test Suite

Run the tests with pytest:

```bash
uv run pytest
```

Tests use mock objects -- no live routers required.

## Contributing

Contributions are welcome. Fork the repository, submit a pull request, and include tests where applicable.

## License

This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for details.
