VoIP & Telephony
The VoIP & Telephony module (id: voip, v1.0.0) delivers two complementary capabilities under one interface:
- Phone fleet management - discover, onboard, provision, reboot, factory-reset, and monitor SIP desk phones; push SIP credentials; track firmware compliance across the fleet.
- PBX integration - connect to a FreePBX or Asterisk PBX and manage extensions, trunks, ring groups, queues, IVR menus, DIDs, active calls, and call-detail records (CDR) without touching the PBX admin panel.
All API paths in this module are prefixed with /api/v1/voip.
Supported adapters
Section titled “Supported adapters”| Adapter | Transport | Notes |
|---|---|---|
| Grandstream | CGI/HTTP (challenge-response; CookieJar(unsafe=True) for IP URLs) | Phone lifecycle, provisioning, live-status, reboot, factory-reset, push-SIP |
| FreePBX / Asterisk | AMI + ARI + REST/OAuth2 | Full PBX management; Asterisk-only (no FreeSWITCH, 3CX, or other PBX types) |
| Yealink | HTTP | Discovery probe + provisioning XML generation (_generate_yealink_xml) - reboot, factory-reset, live-status, and push-SIP-config are Grandstream-only |
Discovery also detects Polycom phones by MAC OUI, but no adapter ships for Polycom.
Permissions
Section titled “Permissions”| Permission code | What it gates | Effective minimum role |
|---|---|---|
voip.view | Read phones, templates, fleet, PBX, voicemail | viewer |
voip.manage_phones | Add/edit/provision phones; PBX create/update/delete; trunk/extension writes; discovery scans; template CRUD | operator |
voip.manage_extensions | Create/delete ring groups | operator |
voip.manage_pbx | Declared; gates nav visibility only - PBX writes actually require voip.manage_phones | operator |
voip.view_calls | CDR search and active-call list | viewer |
voip.discovery | Declared; discovery write endpoints actually require voip.manage_phones | operator |
Two fleet operations are role-gated regardless of permissions:
| Operation | Minimum role |
|---|---|
POST /fleet/bulk/reboot | site_admin |
POST /fleet/bulk/firmware | site_admin |
Phone fleet (Grandstream)
Section titled “Phone fleet (Grandstream)”Phone lifecycle states
Section titled “Phone lifecycle states”Phones move through a defined lifecycle. You cannot skip states.
discovered → onboarding → managed → maintenance → firmware_updating → decommissionedStatus values (real-time): online, offline, ringing, in_call, dnd.
Provision status: pending, generated, pushed, applied, failed, stale.
Connecting a phone
Section titled “Connecting a phone”Before you can reboot, factory-reset, push SIP config, or read live status, a phone needs saved admin web credentials. Use the per-phone test-connection endpoint with save_credentials: true - this is the only endpoint that persists credentials to the phone record.
# Test connection and save credentials (phone record must already exist)curl -s -X POST https://<freesdn>/api/v1/voip/phones/<phone_id>/test-connection \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "ip_address": "10.0.1.101", "username": "admin", "password": "<web-admin-password>", "save_credentials": true }'To probe a phone before creating a record, use POST /phones/test-connection (no phone_id in the path). That ad-hoc endpoint accepts the same body fields (ip_address, username, password) but ignores save_credentials - it runs the probe and returns without persisting anything. To actually save credentials, you must use the per-phone endpoint above (/{phone_id}/test-connection) with the phone ID in the URL path.
If you skip credential saving, reboot/factory-reset/live-status/push-SIP return:
Phone has no saved admin credentials - run Test Phone Connection with 'Save credentials' enabled firstPhone endpoints
Section titled “Phone endpoints”| Method | Path | Purpose |
|---|---|---|
GET | /phones/ | List phones; filters: site_id, pbx_id, status, lifecycle_state, vendor, config_template_id, search; limit 1-500 (default 50) |
GET | /phones/stats | Count phones by status, lifecycle, vendor |
GET | /phones/{phone_id} | Single phone enriched with extension + PBX name |
POST | /phones/ | Create phone record |
PATCH | /phones/{phone_id} | Update phone; triggers device-registry sync |
DELETE | /phones/{phone_id} | Delete phone (204); removes from device registry |
POST | /phones/{phone_id}/onboard | Promote discovered → managed; assign PBX, extension, template, location, tags |
POST | /phones/{phone_id}/migrate | Move phone to another site (target_site_id, dry_run) |
POST | /phones/{phone_id}/reboot | Reboot via Grandstream adapter (202) - Grandstream only |
POST | /phones/{phone_id}/factory-reset | Factory-reset, wipes all config - Grandstream only |
GET | /phones/{phone_id}/live-status | Quick probe: phone_state, line_status, lockout, ts (~150-300 ms) - Grandstream only |
POST | /phones/{phone_id}/push-sip-config | Push extension SIP creds to phone - Grandstream only |
POST | /phones/{phone_id}/provision | Generate and push provisioning XML; records provision action |
GET | /phones/{phone_id}/config-preview | Preview generated provisioning XML (application/xml) |
POST | /phones/{phone_id}/test-connection | Full probe + login + optional credential save |
POST | /phones/test-connection | Ad-hoc connection test by IP (no existing phone record) |
PUT | /phones/{phone_id}/credentials | Save credentials for a phone |
POST | /phones/{phone_id}/decommission | Move phone to decommissioned lifecycle state |
POST | /phones/{phone_id}/maintenance | Toggle maintenance mode |
POST | /phones/auto-link | Auto-link discovered phones to FreePBX extensions by matching SIP registrar IP and user ID |
Provisioning XML
Section titled “Provisioning XML”Config generation (provisioning.py) writes XML to /data/provisioning/voip (maximum 128 KB per file). Vendor dispatch:
| Vendor value | Builder |
|---|---|
grandstream | _generate_grandstream_xml - full GDMS-style XML |
yealink | _generate_yealink_xml - template-based XML |
| anything else | _generate_generic_xml - minimal fallback |
Templates (ConfigTemplateCreate) carry: sip_settings, network_settings, provisioning_settings, feature_settings, line_key_settings (list), raw_overrides, firmware_version. Set is_default: true to apply a template automatically during discovery onboarding.
Provisioning endpoint auth (zero-touch)
Section titled “Provisioning endpoint auth (zero-touch)”Phones pull their config at boot from:
GET /api/v1/voip/provisioning/cfg{mac_address}.xmlThis endpoint has no cookie or JWT auth - the phone has no session. Instead it uses a two-factor device-auth scheme:
- Source-IP allowlist (LAN path) - the phone’s source IP (resolved from
request.client.host, never fromX-Forwarded-For) must fall inside one of the CIDRs listed inSite.subnets. This is the zero-touch path for phones on the managed LAN. - HMAC fallback - if the IP is outside all site subnets, the request must carry
?sig=<hex>(URL parameter) orX-Provisioning-Signatureheader equal toHMAC-SHA256(SECRET_KEY + ENCRYPTION_SALT, mac.lower()), constant-time compared.
Any failure returns a generic 404 (No config for this device) - the error is the same whether the MAC is unknown or the auth check failed, so an attacker cannot enumerate known phones.
Generate a signed URL for a phone from the onboarding flow using generate_provisioning_signature(mac).
Set provisioning_base_url in Settings → VoIP (e.g., http://freesdn:8000/api/v1/voip/provisioning) so the module can build the correct DHCP option-66 value.
Discovery
Section titled “Discovery”Run a network scan to find phones without manual registration.
# Trigger an async discovery scan (returns 202 + scan_id)curl -s -X POST https://<freesdn>/api/v1/voip/discovery/scan \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "scan_type": "full", "subnet": "10.0.1.0/24", "auto_onboard": false, "credentials": {"username": "admin", "password": "<default-pw>"} }'| Method | Path | Purpose |
|---|---|---|
POST | /discovery/scan | Start async Celery scan (202); credentials are transient and encrypted - never persisted |
GET | /discovery/scans | List scan history |
GET | /discovery/scans/{scan_id} | Full scan results |
GET | /discovery/scans/{scan_id}/status | Lightweight progress poll (phase, percent, last 20 log lines) |
POST | /discovery/scans/{scan_id}/cancel | Cancel running scan (best-effort SIGTERM via Celery revoke) |
DELETE | /discovery/scans/{scan_id} | Delete completed/failed/cancelled scan (204) |
scan_type values: full (all probes), arp, sip, http.
Vendor detection uses MAC OUI prefixes (Grandstream 000b82, c074ad, 7c2f80; Yealink 805ec0, 001565, 805e4a; Polycom 0004f2, 64167f) and HTTP fingerprint paths. Scan credentials are passed transiently and encrypted; they are not written to the database.
Fleet bulk operations
Section titled “Fleet bulk operations”| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /fleet/dashboard | Fleet metrics: counts by lifecycle, vendor, model, firmware, SIP-registered | voip.view |
POST | /fleet/bulk/reboot | Reboot 1-200 phones; all IDs validated org-wide in a single query - rejects the whole batch if any ID is foreign | role site_admin |
POST | /fleet/bulk/provision | Generate provisioning configs for a selection | voip.manage_phones |
POST | /fleet/bulk/firmware | Schedule bulk firmware upgrade (irreversible; supply target_version, schedule_at) | role site_admin |
POST | /fleet/bulk/connect | Bulk credential set + probe + persist for a list of phones | voip.manage_phones |
Firmware tracking
Section titled “Firmware tracking”| Method | Path | Purpose |
|---|---|---|
GET | /firmware/ | List registered firmware versions; filter by vendor, model |
POST | /firmware/ | Register a firmware version (201); download_url is SSRF-validated |
GET | /firmware/compliance | Fleet compliance report - phones not on a tracked stable version |
FirmwareTrackCreate requires vendor, model, version. Optional fields: release_date, changelog (max 10,000 chars), download_url (max 500 chars, SSRF-validated on creation), file_checksum (hex ≤128 chars), file_size_bytes (≤4 GB), is_stable, is_recommended.
Config templates
Section titled “Config templates”Templates define the provisioning XML structure applied to phones of a given vendor and model.
| Method | Path | Purpose |
|---|---|---|
GET | /templates/ | List templates with phone_count per template (single GROUP BY, no N+1) |
GET | /templates/{template_id} | Get one template |
POST | /templates/ | Create template (201) |
PATCH | /templates/{template_id} | Update template |
DELETE | /templates/{template_id} | Delete template (204) |
ConfigTemplateCreate fields: name, vendor (required), model_pattern (regex match against phone model string), is_default, plus JSONB blobs: sip_settings, network_settings, provisioning_settings, feature_settings, line_key_settings (list), raw_overrides, firmware_version.
PBX integration (FreePBX / Asterisk)
Section titled “PBX integration (FreePBX / Asterisk)”Connecting a PBX
Section titled “Connecting a PBX”The FreePBX adapter uses OAuth2 + GraphQL when api_client_id and api_client_secret are set (FreePBX 16+ Admin API). Without them it falls back to web-session + legacy AJAX.
# Register a PBX systemcurl -s -X POST https://<freesdn>/api/v1/voip/pbx/ \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "name": "pbx-main", "ip_address": "10.0.0.5", "pbx_type": "freepbx", "api_port": 443, "api_username": "admin", "api_password": "<pass>", "tls_verify_disabled_acknowledged": false }'tls_verify_disabled_acknowledged must be explicitly set to true if you want to skip TLS verification. Do not set it unless you have a specific reason; leave it false (the default) to enforce certificate validation.
After registering, run a full sync to pull extensions, ring groups, trunks, queues, IVR menus, and DIDs into the FreeSDN database:
# Kick off background sync (202); watch WS events on pbx.sync.*curl -s -X POST https://<freesdn>/api/v1/voip/pbx/<pbx_id>/sync \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>"WebSocket sync stages (subscribe to pbx.sync.* filtered by adapter_id='pbx:<pbx_id>'):
connecting → extensions → ring_groups → live_data → persisting → done
PBX system endpoints
Section titled “PBX system endpoints”| Method | Path | Purpose |
|---|---|---|
GET | /pbx/ | List PBX systems (credentials stripped) |
GET | /pbx/{pbx_id} | Get one PBX (sanitized) |
POST | /pbx/ | Create PBX (201) |
PATCH | /pbx/{pbx_id} | Update PBX |
DELETE | /pbx/{pbx_id} | Delete PBX (204) |
POST | /pbx/test-connection | Test connectivity to a PBX before saving |
POST | /pbx/{pbx_id}/sync | Full background sync (202); returns task_id |
POST | /pbx/{pbx_id}/connect | Full connection test using the adapter |
GET | /pbx/{pbx_id}/dashboard | Composite dashboard (DB counts + live AMI/ARI/REST status) |
GET | /pbx/{pbx_id}/system-info | Real-time system info via adapter |
PBXDashboard shows: ami_connected, ari_connected, rest_available, active_calls, voicemail counts.
PBX inventory reads (live, adapter-backed)
Section titled “PBX inventory reads (live, adapter-backed)”All reads require voip.view unless noted. All adapter responses are sanitized: *_password_enc columns dropped, redact_secrets (camelCase-aware) applied, and URLs in error messages stripped before surfacing.
| Method | Path | Purpose |
|---|---|---|
GET | /pbx/{pbx_id}/extensions | Extensions for this PBX with bound phone info |
GET | /pbx/{pbx_id}/trunks | SIP trunks (secret/auth_password redacted) |
GET | /pbx/{pbx_id}/trunks/{trunk_id} | Single trunk detail |
GET | /pbx/{pbx_id}/queues | Call queues |
GET | /pbx/{pbx_id}/ivrs | IVR menus |
GET | /pbx/{pbx_id}/dids | DIDs / inbound routes |
GET | /pbx/{pbx_id}/active-calls | Real-time active calls - requires voip.view_calls |
GET | /pbx/{pbx_id}/voicemail-boxes | Voicemail boxes |
GET | /pbx/{pbx_id}/config | Full synced config snapshot (redacted) |
GET | /pbx/{pbx_id}/outbound-routes | Outbound routes |
GET | /pbx/{pbx_id}/ring-groups | Ring groups for this PBX |
GET | /pbx/{pbx_id}/call-logs | Live CDR via adapter - requires voip.view_calls; params: start_date, end_date, src (≤64), dst (≤64), limit 1-1000 |
GET | /pbx/{pbx_id}/followme | Follow-Me entries |
GET | /pbx/{pbx_id}/announcements | Announcements |
GET | /pbx/{pbx_id}/paging | Paging/intercom groups |
GET | /pbx/{pbx_id}/daynight | Day/night call-flow controls |
GET | /pbx/{pbx_id}/blacklist | Blacklisted numbers |
GET | /pbx/{pbx_id}/certificates | SSL/TLS certificates |
GET | /pbx/{pbx_id}/admin-users | FreePBX AMP admin users (redacted) |
Extensions
Section titled “Extensions”| Method | Path | Purpose |
|---|---|---|
GET | /pbx/{pbx_id}/extensions/{ext_number} | Single extension (ext_number: digits, 1-20 chars) |
POST | /pbx/{pbx_id}/extensions | Create extension (201) |
PATCH | /pbx/{pbx_id}/extensions/{ext_number} | Update extension |
DELETE | /pbx/{pbx_id}/extensions/{ext_number} | Delete extension (204) |
GET | /extensions/ | List extensions across all PBX systems; filter by site_id |
ExtensionCreate fields - required: extension_number (digits, 1-20 chars), display_name (1-255 chars); optional: caller_id_name, caller_id_number, voicemail_enabled (default true), voicemail_pin (4-10 digits), password (8-128 chars, default null), settings.
Trunks
Section titled “Trunks”| Method | Path | Purpose |
|---|---|---|
POST | /pbx/{pbx_id}/trunks | Create SIP trunk (201) |
PATCH | /pbx/{pbx_id}/trunks/{trunk_id} | Update SIP trunk |
DELETE | /pbx/{pbx_id}/trunks/{trunk_id} | Delete SIP trunk (204) |
Trunk bodies are free-form dicts (PJSIP/chan_sip options vary too much to enumerate). Validation enforces: ≤128 keys, ≤4,096 chars per string value, ≤64 KB total body size.
Ring groups
Section titled “Ring groups”| Method | Path | Purpose |
|---|---|---|
GET | /ring-groups/ | List ring groups; filter by pbx_id, site_id |
POST | /ring-groups/ | Create ring group (201) - requires voip.manage_extensions |
DELETE | /ring-groups/{ring_group_id} | Delete ring group (204) - requires voip.manage_extensions |
RingGroupCreate fields: pbx_id, group_number (digits, 1-20 chars), name, ring_strategy (ringall, ringall-prim, hunt, memoryhunt, firstavail, firstnotonphone, random, rrmemory, rrordered; default ringall), ring_time 1-300 s (default 20), members (list of extension numbers), settings.
Live call control
Section titled “Live call control”| Method | Path | Body | Purpose |
|---|---|---|---|
POST | /pbx/{pbx_id}/call/originate | {extension, destination, caller_id, context} | Originate outbound call; context must match ^from-internal(-additional)?$ |
POST | /pbx/{pbx_id}/call/hangup | {channel} | Hang up an active channel |
POST | /pbx/{pbx_id}/call/transfer | {channel, destination, context} | Transfer active call to another extension |
POST | /pbx/{pbx_id}/reload | - | Apply pending config (equivalent to FreePBX “Apply Config”; briefly interrupts active calls) |
POST | /pbx/{pbx_id}/queue/add-member | {queue_name, interface, member_name} | Add member to a call queue |
POST | /pbx/{pbx_id}/queue/remove-member | {queue_name, interface, member_name} | Remove member from a call queue |
queue_name must match ^[\w\-]+$. The FreePBX adapter enforces allowed_outbound_prefixes when originating calls.
CDR and call history
Section titled “CDR and call history”The module exposes two distinct CDR sources that can diverge:
| Endpoint | Source | Notes |
|---|---|---|
GET /call-logs/ | DB-stored call_logs table | Populated by background sync; filters: pbx_id, start_time, end_time, direction, call_status, caller, callee, site_id; limit 1-1,000 |
GET /call-logs/stats | DB-stored call_logs table | Aggregate stats for a time range |
GET /pbx/{pbx_id}/call-logs | Live adapter CDR | Real-time query to FreePBX; result may differ from DB until next sync |
Call direction values: inbound, outbound, internal.
Call status values: answered, missed, voicemail, failed.
Both CDR endpoints require voip.view_calls.
Voicemail
Section titled “Voicemail”| Method | Path | Purpose |
|---|---|---|
GET | /voicemails/ | List voicemail messages; filter by extension_number, folder, is_read, site_id |
GET | /voicemails/stats | Voicemail statistics |
GET | /voicemails/{vm_id} | Get single voicemail record |
PATCH | /voicemails/{vm_id} | Mark read / move to folder |
POST | /voicemails/{vm_id}/mark-read | Mark as read |
DELETE | /voicemails/{vm_id} | Delete voicemail record (204) |
GET | /voicemails/{vm_id}/download | 501 Not Implemented - see below |
Module settings
Section titled “Module settings”Configure in Settings → VoIP in the UI, or via the module settings API.
| Setting | Default | Notes |
|---|---|---|
default_codec | g711u | One of g711u, g711a, g722, g729, opus |
sip_port | 5060 | SIP signaling port |
rtp_port_start | 10000 | RTP media port range start |
rtp_port_end | 20000 | RTP media port range end |
cdr_retention_days | 90 | 30-365; CDR pruned beyond this |
discovery_default_subnet | "" | Pre-fill in the Discovery UI |
provisioning_base_url | "" | E.g. http://freesdn:8000/api/v1/voip/provisioning; used to build DHCP option-66 values |
auto_provision_on_discovery | false | Auto-push provisioning config when a phone is discovered |
health_check_interval | 300 | Seconds between phone health polls (60-3600) |
The provisioning auth HMAC key derives from SECRET_KEY + ENCRYPTION_SALT. Both must be set in your environment (see Deployment).
Background tasks (Celery)
Section titled “Background tasks (Celery)”The module registers these Celery tasks on startup:
| Task name | Purpose |
|---|---|
voip.sync_phones | Sync phone state from adapters |
voip.sync_cdr | Pull CDR from PBX into DB |
voip.sync_extensions | Sync extensions from PBX |
voip.generate_provisioning_files | Regenerate stale provisioning XML |
voip.poll_extension_states | Poll live extension registration status |
voip.reboot_phone | Async single-phone reboot |
voip.run_discovery_scan | Execute a discovery scan scan (credentials transient, never persisted) |
voip.health_check | Module health probe |
voip.check_firmware_compliance | Compare phone firmware against tracked stable versions |
voip.bulk_reboot | Coordinate fleet-wide reboot |
voip.sync_pbx_full | Full PBX sync backing POST /pbx/{id}/sync |
Fabric integration
Section titled “Fabric integration”The VoIP module exposes one read operation in the Fabric catalog:
voip.phone.live_status- NATIVE tier, inputphone_id, permissionvoip.view
Emitted events (subscribable via Fabric Connections):
pbx.sync.started, pbx.sync.progress, pbx.sync.completed, pbx.sync.failed, pbx.originate_call.ok, pbx.originate_call.failed, pbx.reload.ok, pbx.reload.failed, phone.provision.ok, phone.provision.failed, phone.reboot.ok, phone.reboot.failed.
Backup behavior
Section titled “Backup behavior”The Configuration Backup module captures VoIP configuration automatically:
- Included: PBX records, extensions, ring groups, config templates
- Excluded: all credentials - phone web passwords, PBX API credentials, SIP trunk secrets
This is intentional; credentials in a portable archive are a credential-exfil risk. Restore a backup and then re-enter credentials.
Security notes
Section titled “Security notes”- Credential storage: phone admin web passwords are Fernet-encrypted at rest. PBX API and AMI/ARI credentials are also encrypted. All adapter responses run through
redact_secrets(89 sensitive key patterns, camelCase-aware) before returning to the client. - Cross-tenant isolation: cross-tenant references (a phone, PBX, or extension belonging to another org) return 404 rather than 403 to avoid leaking foreign-row existence. Bulk operations that include any foreign phone ID are rejected with 403 for the entire batch.
- Provisioning endpoint: returns a generic 404 for both unknown MACs and failed auth - the error does not distinguish the two cases.
- Error URL redaction: httpx errors embed PBX URLs with credentials in the message. The API redacts these with
re.sub(r"https?://\S+", ...)before surfacing errors. - FreePBX TLS: you must explicitly set
tls_verify_disabled_acknowledged: trueto skip TLS verification. The default isfalse(verification on). - Outbound dialing: the FreePBX adapter enforces
allowed_outbound_prefixeson call-originate requests as a toll-fraud guard.
Frontend pages
Section titled “Frontend pages”| Page | Route | Notes |
|---|---|---|
| Fleet dashboard | /voip | Default landing; fleet metrics and health at a glance |
| Phone list | /voip/phones | Filters, bulk connect/reboot/provision |
| Phone detail | /voip/phones/:id | Test connection, push SIP, reboot/factory-reset, live status, config preview |
| Discovery | /voip/discovery | Trigger and monitor scans; onboard results |
| Config templates | /voip/templates | Template CRUD |
| Firmware tracking | /voip/firmware | Stub page - use the API for now |
| PBX list | /voip/pbx | PBX system list |
| PBX detail | /voip/pbx/:id | 11 tabs: Overview, Extensions, Trunks, Ring Groups, Queues, IVR, DIDs, Active Calls, Voicemail, Config, Settings |
| Extensions | /voip/extensions | Cross-PBX extension list and ring groups |
| Call logs | /voip/calls | CDR search |
| Voicemail | /voip/voicemail | Voicemail inbox (audio download button disabled - 501) |
Nav visibility is permission-gated: Discovery, Templates, and Firmware require voip.manage_phones; PBX Systems requires voip.manage_pbx; Ring Groups requires voip.manage_extensions; Call Logs and Active Calls require voip.view_calls; the rest require voip.view.
Next steps
Section titled “Next steps”- Deployment - environment configuration - set
SECRET_KEY,ENCRYPTION_SALT,FORWARDED_ALLOW_IPSbefore provisioning phones - Configuration Backup - understand what VoIP data is captured and what is excluded from backups
- Firewall - SIP ALG and port-forwarding considerations if phones are behind NAT
- Permissions reference - full RBAC role hierarchy and how to assign permissions
- Fabric catalog - subscribe to VoIP events in Connections and n8n workflows
All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.