Skip to content

Commit 71bad7d

Browse files
murrantStyleCIBot
andauthored
IPv6: Save link-local addresses (librenms#17314)
* Convert ipv6-addresses to a modern module include link local addresses * Convert jetstream * No test data for eltex-mes Delete them, they seem to use IP-MIB?? Delete discover_process_ipv6() * Add db dump query * Fix module name and test capturing, add jetstream * Some working test data * Lock in more test data * Reject ips without a port_id, they are not associated with any device or port * More working test data * Handle unformatted snmp index * Handle mis-formatted prefix fields * Data missing prefix len in the snmprec or 0.0 * Apply fixes from StyleCI * Lint fixes * style fixes * remove cast statements, should only be "empty" or integers in the database I hope * Hedge against broken mibs * Add timos data * wtf? ciena-sds * ciena-sds needs os discovery run for polling.... --------- Co-authored-by: StyleCI Bot <[email protected]>
1 parent 2170b54 commit 71bad7d

File tree

106 files changed

+51157
-48244
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+51157
-48244
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace LibreNMS\Interfaces\Discovery;
4+
5+
use Illuminate\Support\Collection;
6+
7+
interface Ipv6AddressDiscovery
8+
{
9+
/**
10+
* Discover a Collection of Ipv6Address models.
11+
* Will be keyed by ip, port_id and context_name
12+
* ipv6_network_id is optional and will be filled by the module
13+
* ::1 will be filtered by the module as well
14+
*
15+
* @return \Illuminate\Support\Collection<\App\Models\EntPhysical>
16+
*/
17+
public function discoverIpv6Addresses(): Collection;
18+
}

LibreNMS/Modules/Ipv6Addresses.php

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<?php
2+
3+
namespace LibreNMS\Modules;
4+
5+
use App\Facades\PortCache;
6+
use App\Models\Device;
7+
use App\Models\Ipv6Address;
8+
use App\Models\Ipv6Network;
9+
use App\Observers\ModuleModelObserver;
10+
use Illuminate\Support\Collection;
11+
use Illuminate\Support\Facades\Log;
12+
use LibreNMS\DB\SyncsModels;
13+
use LibreNMS\Exceptions\InvalidIpException;
14+
use LibreNMS\Interfaces\Data\DataStorageInterface;
15+
use LibreNMS\Interfaces\Discovery\Ipv6AddressDiscovery;
16+
use LibreNMS\Interfaces\Module;
17+
use LibreNMS\OS;
18+
use LibreNMS\Polling\ModuleStatus;
19+
use LibreNMS\Util\IP;
20+
use LibreNMS\Util\IPv4;
21+
use LibreNMS\Util\IPv6;
22+
use SnmpQuery;
23+
24+
class Ipv6Addresses implements Module
25+
{
26+
use SyncsModels;
27+
28+
/**
29+
* @inheritDoc
30+
*/
31+
public function dependencies(): array
32+
{
33+
return ['ports'];
34+
}
35+
36+
/**
37+
* @inheritDoc
38+
*/
39+
public function shouldDiscover(OS $os, ModuleStatus $status): bool
40+
{
41+
return $status->isEnabledAndDeviceUp($os->getDevice());
42+
}
43+
44+
/**
45+
* @inheritDoc
46+
*/
47+
public function shouldPoll(OS $os, ModuleStatus $status): bool
48+
{
49+
return false;
50+
}
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
public function discover(OS $os): void
56+
{
57+
$ips = new Collection;
58+
if ($os instanceof Ipv6AddressDiscovery) {
59+
$ips = $os->discoverIpv6Addresses();
60+
}
61+
if ($ips->isEmpty()) {
62+
$ips = $this->discoverIpMib($os->getDevice());
63+
}
64+
if ($ips->isEmpty()) {
65+
$ips = $this->discoverIpv6Mib($os->getDevice());
66+
}
67+
68+
// reject localhost and populate ipv6 networks
69+
$ips = $ips->reject(function (Ipv6Address $ip) {
70+
if (! $ip->port_id) {
71+
Log::debug("Skipping $ip->ipv6_compressed due to no matching port");
72+
73+
return true;
74+
}
75+
76+
return $ip->ipv6_compressed === '::1';
77+
})->each(function (Ipv6Address $ip) {
78+
if ($ip->ipv6_network_id === null && $ip->ipv6_prefixlen > 0 && $ip->ipv6_prefixlen <= 128) {
79+
$network = Ipv6Network::firstOrCreate([
80+
'ipv6_network' => IPv6::parse($ip->ipv6_address)->getNetwork($ip->ipv6_prefixlen),
81+
'context_name' => $ip->context_name,
82+
]);
83+
84+
$ip->ipv6_network_id = $network->ipv6_network_id;
85+
}
86+
});
87+
88+
ModuleModelObserver::observe(Ipv6Address::class);
89+
$this->syncModels($os->getDevice(), 'ipv6', $ips);
90+
}
91+
92+
/**
93+
* @inheritDoc
94+
*/
95+
public function poll(OS $os, DataStorageInterface $datastore): void
96+
{
97+
// no polling
98+
}
99+
100+
/**
101+
* @inheritDoc
102+
*/
103+
public function dataExists(Device $device): bool
104+
{
105+
return $device->ipv6()->exists();
106+
}
107+
108+
/**
109+
* @inheritDoc
110+
*/
111+
public function cleanup(Device $device): int
112+
{
113+
return $device->ipv6()->delete();
114+
}
115+
116+
/**
117+
* @inheritDoc
118+
*/
119+
public function dump(Device $device, string $type): ?array
120+
{
121+
if ($type == 'polling') {
122+
return null;
123+
}
124+
125+
return [
126+
'ipv6_addresses' => $device->ipv6()
127+
->leftJoin('ipv6_networks', 'ipv6_addresses.ipv6_network_id', 'ipv6_networks.ipv6_network_id')
128+
->select(['ipv6_addresses.*', 'ipv6_network', 'ifIndex']) // already joined with ports
129+
->orderBy('ipv6_address')->orderBy('ipv6_prefixlen')->orderBy('ifIndex')->orderBy('ipv6_addresses.context_name')
130+
->get()->map->makeHidden(['ipv6_address_id', 'ipv6_network_id', 'port_id', 'laravel_through_key']),
131+
];
132+
}
133+
134+
private function discoverIpMib(Device $device): Collection
135+
{
136+
$ips = new Collection;
137+
foreach ($device->getVrfContexts() as $context_name) {
138+
$ips = $ips->merge(SnmpQuery::context($context_name)
139+
->enumStrings()
140+
->walk([
141+
'IP-MIB::ipAddressIfIndex.ipv6',
142+
'IP-MIB::ipAddressOrigin.ipv6',
143+
'IP-MIB::ipAddressPrefix.ipv6',
144+
])->mapTable(function ($data, $ipAddressAddrType, $ipAddressAddr = '') use ($context_name, $device) {
145+
try {
146+
Log::debug("Attempting to parse $ipAddressAddr");
147+
$ifIndex = $data['IP-MIB::ipAddressIfIndex'] ?? 0;
148+
$ip = $this->parseIp($ipAddressAddr, $ifIndex);
149+
150+
return new Ipv6Address([
151+
'port_id' => PortCache::getIdFromIfIndex($ifIndex, $device),
152+
'ipv6_address' => $ip->uncompressed(),
153+
'ipv6_compressed' => $ip->compressed(),
154+
'ipv6_prefixlen' => $this->parsePrefix($data['IP-MIB::ipAddressPrefix'] ?? ''),
155+
'ipv6_origin' => $data['IP-MIB::ipAddressOrigin'] ?? 'unknown',
156+
'context_name' => $context_name,
157+
]);
158+
} catch (InvalidIpException $e) {
159+
Log::error('Failed to parse IP: ' . $e->getMessage());
160+
161+
return null;
162+
}
163+
}));
164+
}
165+
166+
return $ips->filter();
167+
}
168+
169+
public function discoverIpv6Mib(Device $device): Collection
170+
{
171+
$ips = new Collection;
172+
foreach ($device->getVrfContexts() as $context_name) {
173+
$ips = $ips->merge(SnmpQuery::walk([
174+
'IPV6-MIB::ipv6AddrPfxLength',
175+
'IPV6-MIB::ipv6AddrType',
176+
])->mapTable(function ($data, $ipv6IfIndex = 0, $ipv6AddrAddress = '') use ($context_name, $device) {
177+
try {
178+
$ip = IPv6::parse($ipv6AddrAddress);
179+
$origin = match ($data['IP-MIB::ipv6AddrType'] ?? null) {
180+
'stateless' => 'linklayer',
181+
'stateful' => 'manual',
182+
'unknown' => 'unknown',
183+
default => 'other',
184+
};
185+
186+
return new Ipv6Address([
187+
'port_id' => PortCache::getIdFromIfIndex($ipv6IfIndex, $device),
188+
'ipv6_address' => $ip->uncompressed(),
189+
'ipv6_compressed' => $ip->compressed(),
190+
'ipv6_prefixlen' => $data['IPV6-MIB::ipv6AddrPfxLength'] ?? '',
191+
'ipv6_origin' => $origin,
192+
'context_name' => $context_name,
193+
]);
194+
} catch (InvalidIpException $e) {
195+
Log::error('Failed to parse IP: ' . $e->getMessage());
196+
197+
return null;
198+
}
199+
}));
200+
}
201+
202+
return $ips->filter();
203+
}
204+
205+
/**
206+
* @throws InvalidIpException
207+
*/
208+
private function parseIp(string $ipAddressAddr, string $ifIndex): IP|IPv4|IPv6|null
209+
{
210+
// mis-formatted showing in dot notation
211+
if (str_contains($ipAddressAddr, '.')) {
212+
$cleanSnmpIp = implode('.', array_slice(explode('.', ltrim($ipAddressAddr, '.')), 0, 16));
213+
$ip = IPv6::fromSnmpString($cleanSnmpIp);
214+
} else {
215+
$cleanHexIp = str_replace(['"', "%$ifIndex"], '', $ipAddressAddr);
216+
$ip = IPv6::fromHexString($cleanHexIp);
217+
}
218+
219+
return $ip;
220+
}
221+
222+
private function parsePrefix(string $prefix): string
223+
{
224+
// prefix len is the last index of the ipAddressPrefixTable, fetch it from the pointer
225+
if (str_contains($prefix, '.')) {
226+
return substr($prefix, strrpos($prefix, '.') + 1);
227+
}
228+
229+
preg_match('/(\d{1,3})]$/', $prefix, $prefix_match);
230+
231+
return $prefix_match[1] ?? 0;
232+
}
233+
}

LibreNMS/OS/Jetstream.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace LibreNMS\OS;
4+
5+
use App\Facades\PortCache;
6+
use App\Models\Ipv6Address;
7+
use Illuminate\Support\Collection;
8+
use Illuminate\Support\Facades\Log;
9+
use LibreNMS\Exceptions\InvalidIpException;
10+
use LibreNMS\Interfaces\Discovery\Ipv6AddressDiscovery;
11+
use LibreNMS\OS;
12+
use LibreNMS\Util\IPv6;
13+
14+
class Jetstream extends OS implements Ipv6AddressDiscovery
15+
{
16+
public function discoverIpv6Addresses(): Collection
17+
{
18+
return \SnmpQuery::enumStrings()->walk('TPLINK-IPV6ADDR-MIB::ipv6ParaConfigAddrTable')
19+
->mapTable(function ($data, $ipv6ParaConfigIfIndex, $ipv6ParaConfigAddrType, $ipv6ParaConfigSourceType, $ipv6ParaConfigAddress) {
20+
try {
21+
$ip = IPv6::fromHexString($data['TPLINK-IPV6ADDR-MIB::ipv6ParaConfigAddress']);
22+
23+
// map to IP-MIB origin
24+
$origin = match ($ipv6ParaConfigSourceType) {
25+
'assignedIp' => 'manual',
26+
'autoIp', 'assignedEUI64Ip', 'assignedLinklocalIp', 'negotiate' => 'linklayer',
27+
'dhcpv6' => 'dhcp',
28+
default => 'other',
29+
};
30+
31+
return new Ipv6Address([
32+
'ipv6_address' => $ip->uncompressed(),
33+
'ipv6_compressed' => $ip->compressed(),
34+
'ipv6_prefixlen' => $data['TPLINK-IPV6ADDR-MIB::ipv6ParaConfigPrefixLength'] ?? '',
35+
'ipv6_origin' => $origin,
36+
'port_id' => PortCache::getIdFromIfIndex($ipv6ParaConfigIfIndex, $this->getDevice()),
37+
]);
38+
} catch (InvalidIpException $e) {
39+
Log::error('Failed to parse IP: ' . $e->getMessage());
40+
41+
return null;
42+
}
43+
})->filter();
44+
}
45+
}

app/Models/Ipv6Address.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727
namespace App\Models;
2828

2929
use Illuminate\Database\Eloquent\Relations\BelongsTo;
30+
use LibreNMS\Interfaces\Models\Keyable;
3031

31-
class Ipv6Address extends PortRelatedModel
32+
class Ipv6Address extends PortRelatedModel implements Keyable
3233
{
3334
public $timestamps = false;
3435
protected $primaryKey = 'ipv6_address_id';
@@ -46,4 +47,9 @@ public function network(): BelongsTo
4647
{
4748
return $this->belongsTo(Ipv6Network::class, 'ipv6_network_id', 'ipv6_network_id');
4849
}
50+
51+
public function getCompositeKey(): string
52+
{
53+
return "$this->ipv6_address-$this->ipv6_prefixlen-$this->port_id-$this->context_name";
54+
}
4955
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
// Clean invalid values in ipv6_network_id
15+
DB::table('ipv6_addresses')
16+
->whereNull('ipv6_network_id')
17+
->orWhere('ipv6_network_id', '')
18+
->update(['ipv6_network_id' => 0]);
19+
20+
Schema::table('ipv6_addresses', function (Blueprint $table) {
21+
$table->unsignedInteger('ipv6_network_id')->default(0)->change();
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*/
28+
public function down(): void
29+
{
30+
Schema::table('ipv6_addresses', function (Blueprint $table) {
31+
$table->string('ipv6_network_id', 128)->change();
32+
});
33+
}
34+
};

0 commit comments

Comments
 (0)