Subversion Repositories web_pages

Rev

Blame | Last modification | View Log | Download | RSS feed

# opnsense.pm - Developer's Guide

## Table of Contents

- [Overview](#overview)
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [API Methods](#api-methods)
- [Advanced Topics](#advanced-topics)
- [Error Handling](#error-handling)
- [Examples](#examples)
- [API Endpoints Reference](#api-endpoints-reference)
- [Troubleshooting](#troubleshooting)

## Overview

`opnsense.pm` is a Perl module that provides an object-oriented interface to the OPNsense router API. It simplifies interaction with OPNsense routers for retrieving VPN configurations, user credentials, TOTP secrets, and managing OpenVPN export functionality.

### Features

- Object-oriented interface for OPNsense API
- Dual HTTP client support (curl and LWP::UserAgent)
- SSL verification disabled for self-signed certificates
- Automatic JSON encoding/decoding
- XML configuration parsing
- Complete OpenVPN export functionality

### Version

Current version: 1.0.1

## Installation

### Prerequisites

```bash
# Install required Perl modules
cpan install LWP::UserAgent
cpan install JSON
cpan install XML::Simple
cpan install Data::Dumper
```

Or using system package manager:

```bash
# Debian/Ubuntu
apt-get install libwww-perl libjson-perl libxml-simple-perl

# RedHat/CentOS
yum install perl-libwww-perl perl-JSON perl-XML-Simple
```

### Module Setup

1. Place `opnsense.pm` in your script directory or Perl library path
2. Ensure the module is readable by the user running your scripts
3. For production, consider installing in standard Perl library location

```bash
# Copy to local lib directory
mkdir -p ~/perl5/lib
cp opnsense.pm ~/perl5/lib/

# Or system-wide (requires root)
cp opnsense.pm /usr/local/lib/perl5/site_perl/
```

## Basic Usage

### Creating an API Client

```perl
#!/usr/bin/env perl
use strict;
use warnings;
use opnsense;

# Create OPNsense API client
my $opn = opnsense->new(
    url       => 'https://192.168.1.1',      # Router URL
    apiKey    => 'your-api-key-here',        # From OPNsense UI
    apiSecret => 'your-api-secret-here',     # From OPNsense UI
    ovpnIndex => '1',                        # VPN provider ID
    template  => 'PlainOpenVPN',             # Template type
    hostname  => 'vpn.example.com',          # External hostname
    localPort => '1194'                      # VPN port
);
```

### Configuration Options

```perl
# Use curl instead of LWP (default: 1)
$opnsense::useCurl = 1;

# Enable debug output (default: 0)
$opnsense::debug = 1;
```

### Simple Example

```perl
use opnsense;

my $opn = opnsense->new(
    url       => 'https://192.168.1.1',
    apiKey    => $api_key,
    apiSecret => $api_secret,
    ovpnIndex => '1',
    template  => 'PlainOpenVPN',
    hostname  => 'vpn.example.com',
    localPort => '1194'
);

# Get all VPN users
my $vpnUsers = $opn->getVpnUsers();
print "VPN Users:\n";
foreach my $cert (keys %$vpnUsers) {
    print "  Certificate: $cert, User: $vpnUsers->{$cert}\n";
}
```

## API Methods

### Constructor: new()

Creates a new OPNsense API client object.

```perl
my $opn = opnsense->new(%parameters);
```

**Parameters:**

| Parameter | Type   | Required | Description |
|-----------|--------|----------|-------------|
| url       | string | Yes      | Base URL of OPNsense router (https://...) |
| apiKey    | string | Yes      | API key from System → Access → Users |
| apiSecret | string | Yes      | API secret from System → Access → Users |
| ovpnIndex | string | Yes      | OpenVPN provider index (digit or hash) |
| template  | string | Yes      | OpenVPN template (usually 'PlainOpenVPN') |
| hostname  | string | Yes      | External VPN hostname for client configs |
| localPort | string | Yes      | Port number for VPN connections |

**Returns:** opnsense object

**Example:**
```perl
my $opn = opnsense->new(
    url       => 'https://192.168.1.1',
    apiKey    => 'abc123...',
    apiSecret => 'xyz789...',
    ovpnIndex => '1',
    template  => 'PlainOpenVPN',
    hostname  => 'vpn.example.com',
    localPort => '1194'
);
```

### getVpnProviders()

Retrieves list of OpenVPN providers (instances) configured on the router.

```perl
my $providers = $opn->getVpnProviders();
```

**Returns:** Hashref keyed by provider ID, value is provider name

**Example:**
```perl
my $providers = $opn->getVpnProviders();
foreach my $id (keys %$providers) {
    print "Provider ID: $id, Name: $providers->{$id}\n";
}

# Output:
# Provider ID: 1, Name: Main VPN Server
# Provider ID: 2c3f5a1b, Name: Secondary VPN
```

### getVpnUsers()

Retrieves VPN user certificates from the OPNsense router.

```perl
my $vpnUsers = $opn->getVpnUsers();
```

**Returns:** Hashref keyed by certificate ID, value is username

**Example:**
```perl
my $vpnUsers = $opn->getVpnUsers();
foreach my $cert (keys %$vpnUsers) {
    my $username = $vpnUsers->{$cert};
    print "Certificate: $cert, User: $username\n";
}

# Output:
# Certificate: 60cc1de5e6dfd, User: john
# Certificate: 6327c3d037f8e, User: mary
```

**Notes:**
- Skips users with spaces in username
- Uses 'users' field if available, falls back to 'description' for compatibility

### getAllUsers()

Retrieves all system users including TOTP secrets and authentication data.

```perl
my $allUsers = $opn->getAllUsers();
```

**Returns:** Hashref keyed by username, value is user object

**User Object Structure:**
```perl
{
    'john' => {
        'name'        => 'john',
        'password'    => '<bcrypt hash>',
        'otp_seed'    => 'BASE32SECRET',
        'disabled'    => 0,
        'description' => 'John Doe',
        'email'       => 'john@example.com',
        # ... additional fields
    }
}
```

**Example:**
```perl
my $allUsers = $opn->getAllUsers();
foreach my $username (keys %$allUsers) {
    my $user = $allUsers->{$username};
    my $otp = $user->{otp_seed} || 'Not set';
    my $disabled = $user->{disabled} ? 'Yes' : 'No';
    print "User: $username, OTP: $otp, Disabled: $disabled\n";
}
```

**Important Notes:**
- Downloads entire router configuration via backup API
- Parses XML to extract user data
- Required because /api/system/user endpoint is broken in some versions
- Use sparingly as it's resource-intensive

### getVpnConfig()

Downloads OpenVPN configuration file for a specific certificate.

```perl
my $vpnConfig = $opn->getVpnConfig($certificateId);
```

**Parameters:**

| Parameter     | Type   | Required | Description |
|---------------|--------|----------|-------------|
| certificateId | string | Yes      | Certificate ID from getVpnUsers() |

**Returns:** Hashref containing base64-encoded .ovpn file

**Response Structure:**
```perl
{
    'content' => '<base64 encoded .ovpn configuration>',
    # ... other metadata
}
```

**Example:**
```perl
use MIME::Base64 qw(decode_base64);

my $cert = '60cc1de5e6dfd';
my $vpnConfig = $opn->getVpnConfig($cert);

if ($vpnConfig && $vpnConfig->{content}) {
    # Decode base64 content
    my $ovpnContent = decode_base64($vpnConfig->{content});
    
    # Write to file
    open my $fh, '>', "user_$cert.ovpn" or die $!;
    print $fh $ovpnContent;
    close $fh;
    
    print "VPN config written to user_$cert.ovpn\n";
}
```

### apiRequest() (Internal)

Low-level method for making API requests. Generally not called directly.

```perl
my $response = $opn->apiRequest($endpoint, $method, $data);
```

**Parameters:**

| Parameter | Type    | Required | Description |
|-----------|---------|----------|-------------|
| endpoint  | string  | Yes      | API endpoint path |
| method    | string  | No       | HTTP method (GET/POST/PUT/DELETE), default: GET |
| data      | hashref | No       | Data to send with POST/PUT |

**Returns:** Decoded JSON response or XML string

## Advanced Topics

### Choosing HTTP Client

The module supports two HTTP clients:

**curl (Default):**
```perl
$opnsense::useCurl = 1;
```
- Uses system curl command
- More reliable with self-signed certificates
- Requires curl installed on system
- Recommended for production

**LWP::UserAgent:**
```perl
$opnsense::useCurl = 0;
```
- Pure Perl solution
- No external dependencies
- May have issues with some SSL configurations
- Good for development/testing

### Debug Mode

Enable detailed debugging output:

```perl
$opnsense::debug = 1;

my $providers = $opn->getVpnProviders();
# Outputs API request details, responses, etc.
```

### Custom API Requests

For endpoints not covered by built-in methods:

```perl
# GET request
my $result = $opn->apiRequest('/api/some/endpoint');

# POST request with data
my $payload = {
    field1 => 'value1',
    field2 => 'value2'
};
my $result = $opn->apiRequest('/api/some/endpoint', 'POST', $payload);

# PUT request
my $result = $opn->apiRequest('/api/some/endpoint', 'PUT', $payload);
```

### Working with Multiple Routers

```perl
my @routers = (
    {
        name      => 'router1',
        url       => 'https://192.168.1.1',
        apiKey    => 'key1',
        apiSecret => 'secret1',
        # ... other params
    },
    {
        name      => 'router2',
        url       => 'https://192.168.2.1',
        apiKey    => 'key2',
        apiSecret => 'secret2',
        # ... other params
    }
);

foreach my $router (@routers) {
    my $opn = opnsense->new(%$router);
    my $users = $opn->getVpnUsers();
    print "Router $router->{name} has " . scalar(keys %$users) . " VPN users\n";
}
```

## Error Handling

### Basic Error Handling

```perl
use Try::Tiny;

try {
    my $opn = opnsense->new(%params);
    my $users = $opn->getAllUsers();
    # Process users...
} catch {
    warn "Error connecting to OPNsense: $_";
};
```

### Checking Results

```perl
my $vpnUsers = $opn->getVpnUsers();

if (!$vpnUsers || ref($vpnUsers) ne 'HASH') {
    die "Failed to retrieve VPN users\n";
}

if (keys %$vpnUsers == 0) {
    warn "No VPN users found\n";
}
```

### API Request Failures

```perl
# LWP mode will die on failure
eval {
    my $result = $opn->apiRequest('/api/endpoint');
};
if ($@) {
    print "API request failed: $@\n";
}

# curl mode returns empty/undef on failure
my $result = $opn->apiRequest('/api/endpoint');
if (!defined $result) {
    die "API request returned no data\n";
}
```

## Examples

### Complete VPN User Export

```perl
#!/usr/bin/env perl
use strict;
use warnings;
use opnsense;
use MIME::Base64 qw(decode_base64);
use Data::Dumper;

# Configuration
my $config = {
    url       => 'https://192.168.1.1',
    apiKey    => 'your-api-key',
    apiSecret => 'your-api-secret',
    ovpnIndex => '1',
    template  => 'PlainOpenVPN',
    hostname  => 'vpn.example.com',
    localPort => '1194'
};

# Create API client
my $opn = opnsense->new(%$config);

# Get VPN users (certificate ID => username mapping)
my $vpnUsers = $opn->getVpnUsers();
print "Found " . scalar(keys %$vpnUsers) . " VPN users\n";

# Get all user details including TOTP
my $allUsers = $opn->getAllUsers();

# Process each VPN user
foreach my $cert (keys %$vpnUsers) {
    my $username = $vpnUsers->{$cert};
    my $userDetails = $allUsers->{$username};
    
    next unless $userDetails;
    next if $userDetails->{disabled};
    
    print "\nProcessing user: $username (cert: $cert)\n";
    
    # Get TOTP seed
    my $otpSeed = $userDetails->{otp_seed} || '';
    print "  OTP Seed: " . ($otpSeed ? $otpSeed : "Not configured") . "\n";
    
    # Download VPN config
    my $vpnConfig = $opn->getVpnConfig($cert);
    if ($vpnConfig && $vpnConfig->{content}) {
        my $ovpnContent = decode_base64($vpnConfig->{content});
        
        # Write to file
        my $filename = "${username}_${cert}.ovpn";
        open my $fh, '>', $filename or die "Cannot write $filename: $!";
        print $fh $ovpnContent;
        close $fh;
        
        print "  VPN config: $filename\n";
    }
}

print "\nExport complete!\n";
```

### List All VPN Providers

```perl
#!/usr/bin/env perl
use strict;
use warnings;
use opnsense;

my $opn = opnsense->new(
    url       => 'https://192.168.1.1',
    apiKey    => $ENV{OPNSENSE_API_KEY},
    apiSecret => $ENV{OPNSENSE_API_SECRET},
    ovpnIndex => '1',
    template  => 'PlainOpenVPN',
    hostname  => 'vpn.example.com',
    localPort => '1194'
);

my $providers = $opn->getVpnProviders();

print "Available OpenVPN Providers:\n";
print "=" x 50 . "\n";

foreach my $id (sort keys %$providers) {
    printf "%-15s : %s\n", $id, $providers->{$id};
}
```

### User Audit Report

```perl
#!/usr/bin/env perl
use strict;
use warnings;
use opnsense;

my $opn = opnsense->new(%config);

my $vpnUsers = $opn->getVpnUsers();
my $allUsers = $opn->getAllUsers();

print "VPN User Audit Report\n";
print "=" x 70 . "\n";
printf "%-20s %-15s %-10s %-20s\n", 
    "Username", "Status", "Has TOTP", "Email";
print "-" x 70 . "\n";

foreach my $username (sort keys %$allUsers) {
    my $user = $allUsers->{$username};
    my $hasVpn = 0;
    
    # Check if user has VPN access
    foreach my $cert (keys %$vpnUsers) {
        $hasVpn = 1 if $vpnUsers->{$cert} eq $username;
    }
    
    next unless $hasVpn;
    
    my $status = $user->{disabled} ? "Disabled" : "Active";
    my $hasTotp = $user->{otp_seed} ? "Yes" : "No";
    my $email = $user->{email} || "N/A";
    
    printf "%-20s %-15s %-10s %-20s\n",
        $username, $status, $hasTotp, $email;
}
```

## API Endpoints Reference

### Endpoints Used by Module

| Endpoint | Method | Purpose | Module Method |
|----------|--------|---------|---------------|
| `/api/openvpn/export/providers` | GET | List VPN providers | getVpnProviders() |
| `/api/openvpn/export/accounts/{id}` | GET | List VPN users/certs | getVpnUsers() |
| `/api/core/backup/download/this` | GET | Download config (XML) | getAllUsers() |
| `/api/openvpn/export/download/{id}/{cert}` | POST | Download VPN config | getVpnConfig() |

### Authentication

All requests use HTTP Basic Authentication:
```
Authorization: key <apiKey>:<apiSecret>
```

### SSL/TLS

SSL verification is disabled to support self-signed certificates:
- curl: `--insecure` flag
- LWP: `SSL_verify_mode => 0`

## Troubleshooting

### Common Issues

**1. SSL Certificate Errors**

If using LWP and encountering SSL errors:
```perl
# Switch to curl
$opnsense::useCurl = 1;
```

**2. Empty Response from API**

Enable debug mode to see raw API responses:
```perl
$opnsense::debug = 1;
my $result = $opn->getVpnUsers();
```

**3. "API request failed" Error**

Check:
- Router is accessible from your network
- API credentials are correct
- API access is enabled in OPNsense (System → Settings → Administration)
- User has necessary permissions

**4. No VPN Users Returned**

Verify:
- `ovpnIndex` matches an active VPN provider
- Users have VPN certificates assigned
- Users are not disabled

**5. getAllUsers() Returns Empty**

This endpoint requires admin privileges. Ensure API user has full admin access.

### Debug Output

Example debug session:
```perl
$opnsense::debug = 1;
$opnsense::useCurl = 1;

my $opn = opnsense->new(%config);
my $users = $opn->getVpnUsers();

# Outputs:
# In apiRequestCurl, command is:
#  curl -s --insecure --request GET --user 'key:secret' https://...
```

### Testing Connection

```perl
#!/usr/bin/env perl
use strict;
use warnings;
use opnsense;

my $opn = eval {
    opnsense->new(
        url       => 'https://192.168.1.1',
        apiKey    => 'test-key',
        apiSecret => 'test-secret',
        ovpnIndex => '1',
        template  => 'PlainOpenVPN',
        hostname  => 'vpn.example.com',
        localPort => '1194'
    );
};

if ($@) {
    die "Failed to create API client: $@\n";
}

print "Testing API connection...\n";

my $providers = $opn->getVpnProviders();
if ($providers && keys %$providers) {
    print "✓ Connection successful!\n";
    print "Found " . scalar(keys %$providers) . " VPN provider(s)\n";
} else {
    print "✗ Connection failed or no providers found\n";
}
```

## Performance Considerations

### Caching Results

For frequent access, cache results to minimize API calls:

```perl
my %cache;

sub getCachedUsers {
    my ($opn, $cache_time) = @_;
    $cache_time ||= 300; # 5 minutes
    
    my $now = time;
    if (!$cache{users} || ($now - $cache{timestamp}) > $cache_time) {
        $cache{users} = $opn->getAllUsers();
        $cache{timestamp} = $now;
    }
    
    return $cache{users};
}
```

### Parallel Processing

Use forking or threading for multiple routers:

```perl
use Parallel::ForkManager;

my $pm = Parallel::ForkManager->new(5);

foreach my $router (@routers) {
    $pm->start and next;
    
    my $opn = opnsense->new(%$router);
    my $users = $opn->getVpnUsers();
    # Process users...
    
    $pm->finish;
}

$pm->wait_all_children;
```

## Security Best Practices

1. **Store API credentials securely:**
   ```perl
   # Use environment variables
   my $apiKey = $ENV{OPNSENSE_API_KEY};
   my $apiSecret = $ENV{OPNSENSE_API_SECRET};
   
   # Or encrypted configuration files
   # Never hardcode credentials in scripts
   ```

2. **Use dedicated API keys:**
   - Create separate API users for each application
   - Grant minimum necessary permissions
   - Rotate keys regularly

3. **Protect configuration files:**
   ```bash
   chmod 600 routers.json
   chown root:root routers.json
   ```

4. **Use HTTPS:**
   - Always connect via HTTPS
   - Consider proper SSL certificates for production

5. **Audit access:**
   - Log all API access
   - Monitor for unusual activity
   - Review OPNsense logs regularly

## Support and Contributing

### Getting Help

- Check OPNsense API documentation: https://docs.opnsense.org/
- Review module source code for implementation details
- Enable debug mode for troubleshooting

### Reporting Issues

When reporting issues, include:
- OPNsense version
- Perl version (`perl -v`)
- Module version
- Debug output (sanitize credentials!)
- Complete error messages

### Contributing

Improvements welcome! Consider:
- Additional API endpoint methods
- Better error handling
- Performance optimizations
- Documentation improvements

## License

Copyright (c) 2025, Daily Data, Inc.
All rights reserved.

This software is provided under the BSD 3-Clause License.

---

**Last Updated:** January 4, 2026  
**Version:** 1.0.1