Skip to content

Commit 73adfce

Browse files
feat: add WireGuard status endpoints #790
1 parent fb65fd6 commit 73adfce

5 files changed

Lines changed: 356 additions & 1 deletion

File tree

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1769,7 +1769,7 @@ class Model {
17691769
* @throws NotFoundError When the Model requires a pfSense package that is not installed.
17701770
* @throws ServerError When a package requires a PHP include file that could not be found.
17711771
*/
1772-
private function check_packages(): void {
1772+
protected function check_packages(): void {
17731773
# Check if the user has opted in to using development (-devel) package variants
17741774
$pkg_config = RESTAPI\Models\RESTAPISettings::get_pkg_config();
17751775
$allow_development_packages = ($pkg_config['allow_development_packages'] ?? '') === 'enabled';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace RESTAPI\Endpoints;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Core\Endpoint;
8+
9+
/**
10+
* Defines an Endpoint for interacting with multiple WireGuard peer status objects at /api/v2/status/wireguard/peers.
11+
*/
12+
class StatusWireGuardPeersEndpoint extends Endpoint {
13+
public function __construct() {
14+
# Set Endpoint attributes
15+
$this->url = '/api/v2/status/wireguard/peers';
16+
$this->model_name = 'WireGuardPeerStatus';
17+
$this->many = true;
18+
$this->request_method_options = ['GET'];
19+
20+
# Construct the parent Endpoint object
21+
parent::__construct();
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace RESTAPI\Endpoints;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Core\Endpoint;
8+
9+
/**
10+
* Defines an Endpoint for interacting with multiple WireGuard tunnel status objects at /api/v2/status/wireguard/tunnels.
11+
*/
12+
class StatusWireGuardTunnelsEndpoint extends Endpoint {
13+
public function __construct() {
14+
# Set Endpoint attributes
15+
$this->url = '/api/v2/status/wireguard/tunnels';
16+
$this->model_name = 'WireGuardTunnelStatus';
17+
$this->many = true;
18+
$this->request_method_options = ['GET'];
19+
20+
# Construct the parent Endpoint object
21+
parent::__construct();
22+
}
23+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace RESTAPI\Models;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Core\Model;
8+
use RESTAPI\Fields\IntegerField;
9+
use RESTAPI\Fields\StringField;
10+
use RESTAPI\Fields\UnixTimeField;
11+
12+
/**
13+
* Defines a Model that represents the live status of a single WireGuard peer. This Model is used as a nested object
14+
* within WireGuardTunnelStatus and is read-only (backed by wg_get_status() via the parent model).
15+
*/
16+
class WireGuardPeerStatus extends Model {
17+
public StringField $tunnel_device;
18+
public StringField $public_key;
19+
public StringField $preshared_key;
20+
public StringField $endpoint;
21+
public StringField $allowed_ips;
22+
public UnixTimeField $latest_handshake;
23+
public IntegerField $transfer_rx;
24+
public IntegerField $transfer_tx;
25+
public StringField $persistent_keepalive;
26+
public StringField $descr;
27+
28+
public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) {
29+
# Model attributes
30+
$this->many = true;
31+
$this->internal_callable = 'get_wireguard_status';
32+
$this->verbose_name = 'WireGuard Tunnel Status Peer';
33+
$this->packages = ['pfSense-pkg-WireGuard'];
34+
$this->package_includes = [
35+
'wireguard/includes/wg_service.inc',
36+
'wireguard/includes/wg.inc',
37+
'wireguard/includes/wg_globals.inc',
38+
'wireguard/includes/wg_api.inc',
39+
];
40+
41+
# Fields
42+
$this->tunnel_device = new StringField(
43+
default: null,
44+
allow_null: true,
45+
read_only: true,
46+
verbose_name: 'Tunnel Device',
47+
help_text: 'The tunnel device this peer is associated with.',
48+
);
49+
$this->public_key = new StringField(
50+
default: null,
51+
allow_null: true,
52+
read_only: true,
53+
verbose_name: 'Public Key',
54+
help_text: 'The public key of the peer.',
55+
);
56+
$this->preshared_key = new StringField(
57+
default: null,
58+
allow_empty: true,
59+
allow_null: true,
60+
read_only: true,
61+
sensitive: true,
62+
verbose_name: 'Preshared Key',
63+
help_text: 'The preshared key negotiated with this peer, if any.',
64+
);
65+
$this->endpoint = new StringField(
66+
default: null,
67+
allow_empty: true,
68+
allow_null: true,
69+
read_only: true,
70+
verbose_name: 'Endpoint',
71+
help_text: 'The remote endpoint (IP:port) the peer is connected from.',
72+
);
73+
$this->allowed_ips = new StringField(
74+
default: null,
75+
allow_empty: true,
76+
allow_null: true,
77+
read_only: true,
78+
many: true,
79+
verbose_name: 'Allowed IPs',
80+
help_text: 'The comma-separated list of IP prefixes that are routed to this peer.',
81+
);
82+
$this->latest_handshake = new UnixTimeField(
83+
default: null,
84+
allow_null: true,
85+
read_only: true,
86+
verbose_name: 'Latest Handshake',
87+
help_text: 'The Unix timestamp of the most recent handshake with this peer.',
88+
);
89+
$this->transfer_rx = new IntegerField(
90+
default: null,
91+
allow_null: true,
92+
read_only: true,
93+
verbose_name: 'Transfer RX',
94+
help_text: 'The total number of bytes received from this peer.',
95+
);
96+
$this->transfer_tx = new IntegerField(
97+
default: null,
98+
allow_null: true,
99+
read_only: true,
100+
verbose_name: 'Transfer TX',
101+
help_text: 'The total number of bytes sent to this peer.',
102+
);
103+
$this->persistent_keepalive = new StringField(
104+
default: null,
105+
allow_empty: true,
106+
allow_null: true,
107+
read_only: true,
108+
verbose_name: 'Persistent Keepalive',
109+
help_text: 'The persistent keepalive interval for this peer, or "off" if disabled.',
110+
);
111+
$this->descr = new StringField(
112+
default: '',
113+
allow_empty: true,
114+
read_only: true,
115+
verbose_name: 'Description',
116+
help_text: 'The description of this peer as stored in the pfSense configuration.',
117+
);
118+
119+
parent::__construct($id, $parent_id, $data, ...$options);
120+
}
121+
122+
/**
123+
* Retrieves the live WireGuard status from the `wg` CLI tool and maps it into the flat array format
124+
* expected by the Model framework's internal_callable mechanism.
125+
* @return array An indexed array of tunnel status records, each containing peer status as a sub-array.
126+
*/
127+
protected function get_wireguard_status(): array {
128+
$peers = [];
129+
$tunnels = (new WireGuardTunnelStatus())->get_internal_objects();
130+
131+
foreach ($tunnels as $tunnel) {
132+
$peers = $peers + $tunnel['peers'];
133+
}
134+
135+
return $peers;
136+
}
137+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
namespace RESTAPI\Models;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Core\Model;
8+
use RESTAPI\Fields\IntegerField;
9+
use RESTAPI\Fields\NestedModelField;
10+
use RESTAPI\Fields\StringField;
11+
12+
/**
13+
* Defines a Model that represents the live runtime status of a WireGuard tunnel. Data is sourced from wg_get_status()
14+
* and is read-only. Peer status information is embedded via a nested WireGuardTunnelStatusPeer model.
15+
*/
16+
class WireGuardTunnelStatus extends Model {
17+
public StringField $name;
18+
public StringField $status;
19+
public StringField $public_key;
20+
public StringField $listen_port;
21+
public IntegerField $mtu;
22+
public IntegerField $transfer_rx;
23+
public IntegerField $transfer_tx;
24+
public IntegerField $inpkts;
25+
public IntegerField $outpkts;
26+
public StringField $descr;
27+
public NestedModelField $peers;
28+
29+
public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) {
30+
# Model attributes
31+
$this->internal_callable = 'get_wireguard_status';
32+
$this->many = true;
33+
$this->verbose_name = 'WireGuard Tunnel Status';
34+
$this->packages = ['pfSense-pkg-WireGuard'];
35+
$this->package_includes = [
36+
'wireguard/includes/wg_service.inc',
37+
'wireguard/includes/wg.inc',
38+
'wireguard/includes/wg_globals.inc',
39+
'wireguard/includes/wg_api.inc',
40+
];
41+
42+
# Fields
43+
$this->name = new StringField(
44+
default: null,
45+
allow_null: true,
46+
read_only: true,
47+
verbose_name: 'Name',
48+
help_text: 'The WireGuard tunnel interface name (e.g. wg0).',
49+
);
50+
$this->status = new StringField(
51+
default: null,
52+
choices: ['up', 'down'],
53+
allow_null: true,
54+
read_only: true,
55+
verbose_name: 'Status',
56+
help_text: 'The current operational status of the tunnel interface.',
57+
);
58+
$this->public_key = new StringField(
59+
default: null,
60+
allow_null: true,
61+
read_only: true,
62+
verbose_name: 'Public Key',
63+
help_text: 'The public key of this tunnel.',
64+
);
65+
$this->listen_port = new StringField(
66+
default: null,
67+
allow_null: true,
68+
read_only: true,
69+
verbose_name: 'Listen Port',
70+
help_text: 'The UDP port this tunnel is listening on.',
71+
);
72+
$this->mtu = new IntegerField(
73+
default: null,
74+
allow_null: true,
75+
read_only: true,
76+
verbose_name: 'MTU',
77+
help_text: 'The MTU of the tunnel interface.',
78+
);
79+
$this->transfer_rx = new IntegerField(
80+
default: null,
81+
allow_null: true,
82+
read_only: true,
83+
verbose_name: 'Transfer RX',
84+
help_text: 'The total number of bytes received on the tunnel interface.',
85+
);
86+
$this->transfer_tx = new IntegerField(
87+
default: null,
88+
allow_null: true,
89+
read_only: true,
90+
verbose_name: 'Transfer TX',
91+
help_text: 'The total number of bytes transmitted on the tunnel interface.',
92+
);
93+
$this->inpkts = new IntegerField(
94+
default: null,
95+
allow_null: true,
96+
read_only: true,
97+
verbose_name: 'Inbound Packets',
98+
help_text: 'The total number of inbound packets observed on the tunnel interface.',
99+
);
100+
$this->outpkts = new IntegerField(
101+
default: null,
102+
allow_null: true,
103+
read_only: true,
104+
verbose_name: 'Outbound Packets',
105+
help_text: 'The total number of outbound packets observed on the tunnel interface.',
106+
);
107+
$this->descr = new StringField(
108+
default: '',
109+
allow_empty: true,
110+
read_only: true,
111+
verbose_name: 'Description',
112+
help_text: 'The description of this tunnel as stored in the pfSense configuration.',
113+
);
114+
$this->peers = new NestedModelField(
115+
model_class: 'WireGuardPeerStatus',
116+
default: [],
117+
allow_empty: true,
118+
read_only: true,
119+
verbose_name: 'Peers',
120+
help_text: 'The live status of each peer associated with this WireGuard tunnel.',
121+
);
122+
123+
parent::__construct($id, $parent_id, $data, ...$options);
124+
}
125+
126+
/**
127+
* Retrieves the live WireGuard status from the `wg` CLI tool and maps it into the flat array format
128+
* expected by the Model framework's internal_callable mechanism.
129+
* @return array An indexed array of tunnel status records, each containing peer status as a sub-array.
130+
*/
131+
protected function get_wireguard_status(): array {
132+
$tunnels = [];
133+
134+
$this->check_packages();
135+
136+
foreach (wg_get_status() as $device_name => $device) {
137+
# Build the indexed peer list from the public-key-keyed peers array
138+
$peers = [];
139+
140+
foreach ($device['peers'] as $peer) {
141+
$peers[] = [
142+
'tunnel_device' => $device_name,
143+
'public_key' => $peer['public_key'] ?? null,
144+
'preshared_key' => $peer['preshared_key'] ?? null,
145+
'endpoint' => $peer['endpoint'] ?? null,
146+
'allowed_ips' => $peer['allowed_ips'] ?? null,
147+
'latest_handshake' => isset($peer['latest_handshake']) ? (int) $peer['latest_handshake'] : null,
148+
'transfer_rx' => isset($peer['transfer_rx']) ? (int) $peer['transfer_rx'] : null,
149+
'transfer_tx' => isset($peer['transfer_tx']) ? (int) $peer['transfer_tx'] : null,
150+
'persistent_keepalive' => $peer['persistent_keepalive'] ?? null,
151+
'descr' => $peer['config']['descr'] ?? '',
152+
];
153+
}
154+
155+
$tunnels[] = [
156+
'name' => $device_name,
157+
'status' => $device['status'] ?? null,
158+
'public_key' => $device['public_key'] ?? null,
159+
'listen_port' => $device['listen_port'] ?? null,
160+
'mtu' => isset($device['mtu']) ? (int) $device['mtu'] : null,
161+
'transfer_rx' => isset($device['transfer_rx']) ? (int) $device['transfer_rx'] : null,
162+
'transfer_tx' => isset($device['transfer_tx']) ? (int) $device['transfer_tx'] : null,
163+
'inpkts' => isset($device['inpkts']) ? (int) $device['inpkts'] : null,
164+
'outpkts' => isset($device['outpkts']) ? (int) $device['outpkts'] : null,
165+
'descr' => $device['config']['descr'] ?? '',
166+
'peers' => $peers,
167+
];
168+
}
169+
170+
return $tunnels;
171+
}
172+
}

0 commit comments

Comments
 (0)