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