Skip to content

Commit 014df8b

Browse files
fix: deduplicate IPv4/IPv6 and internal port entries (#84)
1 parent 2541ae7 commit 014df8b

File tree

5 files changed

+46
-12
lines changed

5 files changed

+46
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to portracker will be documented in this file.
44

5+
## [1.3.2] - 2026-02-03
6+
7+
### Fixed
8+
9+
- **Duplicate Ports**: Fixed duplicate port entries caused by IPv4/IPv6 wildcard addresses and internal port detection (resolves #82)
10+
511
## [1.3.1] - 2026-02-03
612

713
### Fixed

backend/collectors/base_collector.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,15 @@ class BaseCollector {
139139
* @returns {Object} Normalized port entry
140140
*/
141141
normalizePortEntry(entry) {
142+
let hostIp = entry.host_ip || "0.0.0.0";
143+
if (hostIp === "::" || hostIp === "[::]" || hostIp === "*") {
144+
hostIp = "0.0.0.0";
145+
}
142146
return {
143147
source: entry.source || this.platform,
144148
owner: entry.owner || "unknown",
145149
protocol: entry.protocol || "tcp",
146-
host_ip: entry.host_ip || "0.0.0.0",
150+
host_ip: hostIp,
147151
host_port: parseInt(entry.host_port, 10) || 0,
148152
target: entry.target || null,
149153
container_id: entry.container_id || null,

backend/collectors/docker_collector.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ class DockerCollector extends BaseCollector {
2929
this.dockerApi = new DockerAPIClient();
3030
}
3131

32+
_normalizeHostIp(ip) {
33+
if (!ip || ip === '' || ip === '::' || ip === '::0' || ip === '0.0.0.0') {
34+
return '0.0.0.0';
35+
}
36+
return ip;
37+
}
38+
3239
async initialize() {
3340
return await this.dockerApi.connect();
3441
}
@@ -164,7 +171,8 @@ class DockerCollector extends BaseCollector {
164171

165172
const dockerPorts = await this._getDockerContainerPorts();
166173
dockerPorts.forEach((port) => {
167-
const key = `${port.host_ip}:${port.host_port}:${port.protocol}`;
174+
const normalizedIp = this._normalizeHostIp(port.host_ip);
175+
const key = `${normalizedIp}:${port.host_port}:${port.protocol}`;
168176
if (!dockerPortsMap.has(key)) {
169177
if (port.container_id) {
170178
port.created = containerCreationTimeMap.get(port.container_id) || null;
@@ -184,8 +192,9 @@ class DockerCollector extends BaseCollector {
184192

185193
if (container.internalPorts && container.internalPorts.length > 0) {
186194
container.internalPorts.forEach((internalPort) => {
187-
const publishedKey = `${internalPort.host_ip}:${internalPort.host_port}:${internalPort.protocol}`;
188-
const internalKey = `${internalPort.host_ip}:${internalPort.host_port}:${internalPort.protocol}:${container.id}:internal`;
195+
const normalizedIp = this._normalizeHostIp(internalPort.host_ip);
196+
const publishedKey = `${normalizedIp}:${internalPort.host_port}:${internalPort.protocol}`;
197+
const internalKey = `${normalizedIp}:${internalPort.host_port}:${internalPort.protocol}:${container.id}:internal`;
189198

190199
if (!dockerPortsMap.has(publishedKey) && !dockerPortsMap.has(internalKey)) {
191200
internalPort.created = containerCreationTimeMap.get(container.id) || null;
@@ -203,7 +212,9 @@ class DockerCollector extends BaseCollector {
203212
const systemPorts = await this._getSystemPorts();
204213

205214
for (const port of systemPorts) {
206-
const key = `${port.host_ip}:${port.host_port}`;
215+
// Use same key format as docker ports for proper dedup
216+
const normalizedIp = this._normalizeHostIp(port.host_ip);
217+
const key = `${normalizedIp}:${port.host_port}:${port.protocol || 'tcp'}`;
207218
if (dockerPortsMap.has(key)) {
208219
continue;
209220
}
@@ -301,10 +312,10 @@ class DockerCollector extends BaseCollector {
301312
const targetPort = parseInt(port, 10);
302313

303314
for (const binding of hostBindings) {
304-
const hostIp = binding.HostIp || '0.0.0.0';
315+
const hostIp = this._normalizeHostIp(binding.HostIp);
305316
const hostPort = parseInt(binding.HostPort, 10);
306317

307-
if (!hostIp || isNaN(hostPort)) continue;
318+
if (isNaN(hostPort)) continue;
308319

309320
portEntries.push(
310321
this.normalizePortEntry({

backend/collectors/truenas_collector.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,8 +570,8 @@ class TrueNASCollector extends BaseCollector {
570570
* @returns {string} Resolved IP address
571571
*/
572572
_resolveHostIP(host_ip) {
573-
if (host_ip === "0.0.0.0" || host_ip === "::") {
574-
return host_ip;
573+
if (host_ip === "::" || host_ip === "[::]" || host_ip === "0.0.0.0") {
574+
return "0.0.0.0";
575575
}
576576

577577
switch (host_ip) {
@@ -1119,16 +1119,29 @@ class TrueNASCollector extends BaseCollector {
11191119
await this._buildHostProcToContainerMap();
11201120

11211121
for (const port of dockerPorts) {
1122+
const normalizedIp = this._resolveHostIP(port.host_ip);
1123+
1124+
if (port.internal && port.container_id) {
1125+
const publishedKey = `${normalizedIp}:${port.host_port}`;
1126+
const existingPort = uniquePorts.get(publishedKey);
1127+
if (existingPort && existingPort.container_id === port.container_id) {
1128+
continue;
1129+
}
1130+
}
1131+
11221132
const key = port.internal
11231133
? `${port.container_id}:${port.host_port}:internal`
1124-
: `${port.host_ip}:${port.host_port}`;
1134+
: `${normalizedIp}:${port.host_port}`;
1135+
port.host_ip = normalizedIp;
11251136
port.created =
11261137
containerCreationTimeMap.get(port.container_id) || null;
11271138
uniquePorts.set(key, port);
11281139
}
11291140

11301141
for (const port of systemPorts) {
1131-
const key = `${port.host_ip}:${port.host_port}`;
1142+
const normalizedIp = this._resolveHostIP(port.host_ip);
1143+
const key = `${normalizedIp}:${port.host_port}`;
1144+
port.host_ip = normalizedIp;
11321145
if (uniquePorts.has(key)) {
11331146
const existingPort = uniquePorts.get(key);
11341147
if (!existingPort.pid) existingPort.pid = port.pid;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "portracker",
3-
"version": "1.3.1",
3+
"version": "1.3.2",
44
"description": "A multi-platform port-tracking dashboard",
55
"main": "backend/index.js",
66
"scripts": {

0 commit comments

Comments
 (0)