knx.ts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/@types/interfaces/DPTs.d.ts +144 -0
- package/dist/@types/interfaces/DPTs.js +3 -0
- package/dist/@types/interfaces/EMI.d.ts +396 -0
- package/dist/@types/interfaces/EMI.js +2 -0
- package/dist/@types/interfaces/ServiceMessage.d.ts +11 -0
- package/dist/@types/interfaces/ServiceMessage.js +2 -0
- package/dist/@types/interfaces/SystemStatus.d.ts +18 -0
- package/dist/@types/interfaces/SystemStatus.js +2 -0
- package/dist/@types/interfaces/connection.d.ts +139 -0
- package/dist/@types/interfaces/connection.js +2 -0
- package/dist/@types/interfaces/localEndPoint.d.ts +5 -0
- package/dist/@types/interfaces/localEndPoint.js +2 -0
- package/dist/@types/types/AllDpts.d.ts +3 -0
- package/dist/@types/types/AllDpts.js +3 -0
- package/dist/@types/types/DecodedDPTType.d.ts +21 -0
- package/dist/@types/types/DecodedDPTType.js +2 -0
- package/dist/connection/KNXService.d.ts +58 -0
- package/dist/connection/KNXService.js +242 -0
- package/dist/connection/KNXTunneling.d.ts +44 -0
- package/dist/connection/KNXTunneling.js +509 -0
- package/dist/connection/KNXnetIPServer.d.ts +64 -0
- package/dist/connection/KNXnetIPServer.js +900 -0
- package/dist/connection/Router.d.ts +49 -0
- package/dist/connection/Router.js +269 -0
- package/dist/connection/TPUART.d.ts +32 -0
- package/dist/connection/TPUART.js +497 -0
- package/dist/connection/TunnelConnection.d.ts +57 -0
- package/dist/connection/TunnelConnection.js +167 -0
- package/dist/core/CEMI.d.ts +1130 -0
- package/dist/core/CEMI.js +1281 -0
- package/dist/core/ControlField.d.ts +57 -0
- package/dist/core/ControlField.js +120 -0
- package/dist/core/ControlFieldExtended.d.ts +56 -0
- package/dist/core/ControlFieldExtended.js +114 -0
- package/dist/core/EMI.d.ts +2515 -0
- package/dist/core/EMI.js +3898 -0
- package/dist/core/KNXAddInfoTypes.d.ts +225 -0
- package/dist/core/KNXAddInfoTypes.js +602 -0
- package/dist/core/KNXnetIPHeader.d.ts +10 -0
- package/dist/core/KNXnetIPHeader.js +38 -0
- package/dist/core/KNXnetIPStructures.d.ts +179 -0
- package/dist/core/KNXnetIPStructures.js +622 -0
- package/dist/core/MessageCodeField.d.ts +886 -0
- package/dist/core/MessageCodeField.js +399 -0
- package/dist/core/SystemStatus.d.ts +144 -0
- package/dist/core/SystemStatus.js +325 -0
- package/dist/core/data/KNXData.d.ts +7 -0
- package/dist/core/data/KNXData.js +30 -0
- package/dist/core/data/KNXDataDecode.d.ts +396 -0
- package/dist/core/data/KNXDataDecode.js +1186 -0
- package/dist/core/data/KNXDataEncode.d.ts +332 -0
- package/dist/core/data/KNXDataEncode.js +1504 -0
- package/dist/core/enum/APCIEnum.d.ts +587 -0
- package/dist/core/enum/APCIEnum.js +591 -0
- package/dist/core/enum/EnumControlField.d.ts +24 -0
- package/dist/core/enum/EnumControlField.js +36 -0
- package/dist/core/enum/EnumControlFieldExtended.d.ts +36 -0
- package/dist/core/enum/EnumControlFieldExtended.js +41 -0
- package/dist/core/enum/EnumShortACKFrame.d.ts +6 -0
- package/dist/core/enum/EnumShortACKFrame.js +10 -0
- package/dist/core/enum/ErrorCodeSet.d.ts +57 -0
- package/dist/core/enum/ErrorCodeSet.js +2 -0
- package/dist/core/enum/KNXnetIPEnum.d.ts +95 -0
- package/dist/core/enum/KNXnetIPEnum.js +90 -0
- package/dist/core/enum/SAP.d.ts +19 -0
- package/dist/core/enum/SAP.js +23 -0
- package/dist/core/layers/data/APDU.d.ts +38 -0
- package/dist/core/layers/data/APDU.js +115 -0
- package/dist/core/layers/data/NPDU.d.ts +73 -0
- package/dist/core/layers/data/NPDU.js +103 -0
- package/dist/core/layers/data/TPDU.d.ts +53 -0
- package/dist/core/layers/data/TPDU.js +73 -0
- package/dist/core/layers/interfaces/APCI.d.ts +61 -0
- package/dist/core/layers/interfaces/APCI.js +92 -0
- package/dist/core/layers/interfaces/TPCI.d.ts +110 -0
- package/dist/core/layers/interfaces/TPCI.js +196 -0
- package/dist/core/resources/DeviceDescriptorType.d.ts +46 -0
- package/dist/core/resources/DeviceDescriptorType.js +69 -0
- package/dist/errors/DPTNotFound.d.ts +6 -0
- package/dist/errors/DPTNotFound.js +15 -0
- package/dist/errors/InvalidKnxAddresExeption.d.ts +3 -0
- package/dist/errors/InvalidKnxAddresExeption.js +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +18 -0
- package/dist/utils/CEMIAdapter.d.ts +16 -0
- package/dist/utils/CEMIAdapter.js +94 -0
- package/dist/utils/KNXHelper.d.ts +78 -0
- package/dist/utils/KNXHelper.js +338 -0
- package/dist/utils/Logger.d.ts +17 -0
- package/dist/utils/Logger.js +96 -0
- package/dist/utils/MessageCodeTranslator.d.ts +19 -0
- package/dist/utils/MessageCodeTranslator.js +77 -0
- package/dist/utils/checksumFrame.d.ts +18 -0
- package/dist/utils/checksumFrame.js +41 -0
- package/dist/utils/localIp.d.ts +7 -0
- package/dist/utils/localIp.js +45 -0
- package/package.json +49 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.KNXnetIPServer = void 0;
|
|
7
|
+
const dgram_1 = __importDefault(require("dgram"));
|
|
8
|
+
const KNXService_1 = require("./KNXService");
|
|
9
|
+
const KNXnetIPHeader_1 = require("../core/KNXnetIPHeader");
|
|
10
|
+
const KNXnetIPEnum_1 = require("../core/enum/KNXnetIPEnum");
|
|
11
|
+
const CEMI_1 = require("../core/CEMI");
|
|
12
|
+
const KNXnetIPStructures_1 = require("../core/KNXnetIPStructures");
|
|
13
|
+
const KNXHelper_1 = require("../utils/KNXHelper");
|
|
14
|
+
const localIp_1 = require("../utils/localIp");
|
|
15
|
+
const Router_1 = require("./Router");
|
|
16
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
17
|
+
const DeviceDescriptorType_1 = require("../core/resources/DeviceDescriptorType");
|
|
18
|
+
const TunnelConnection_1 = require("./TunnelConnection");
|
|
19
|
+
const InvalidKnxAddresExeption_1 = require("../errors/InvalidKnxAddresExeption");
|
|
20
|
+
/**
|
|
21
|
+
* Implements a KNXnet/IP Server (Gateway) that supports Routing and Tunneling protocols.
|
|
22
|
+
* This class handles device discovery (Search/Description), manages multiple concurrent
|
|
23
|
+
* tunneling connections, and bridges communication between IP multicast (Routing) and
|
|
24
|
+
* point-to-point (Tunneling) clients. It includes implementation for flow control
|
|
25
|
+
* (RoutingBusy), rate limiting, and echo cancellation.
|
|
26
|
+
*/
|
|
27
|
+
class KNXnetIPServer extends KNXService_1.KNXService {
|
|
28
|
+
isRoutingBusy = false;
|
|
29
|
+
routingBusyTimer = null;
|
|
30
|
+
msgQueue = [];
|
|
31
|
+
isProcessingQueue = false;
|
|
32
|
+
lastSentTime = 0;
|
|
33
|
+
busyCounter = 0; // N for random wait time calculation
|
|
34
|
+
lastBusyTime = 0;
|
|
35
|
+
decrementTimer = null;
|
|
36
|
+
decrementInterval = null;
|
|
37
|
+
// [MEJORA] Almacenamos la IA en formato entero para el filtro anti-eco rápido
|
|
38
|
+
serverIAInt;
|
|
39
|
+
_tunnelConnections = new Map();
|
|
40
|
+
MAX_QUEUE_SIZE = 100;
|
|
41
|
+
BUSY_THRESHOLD = 15;
|
|
42
|
+
HEARTBEAT_TIMEOUT = KNXnetIPEnum_1.KNXTimeoutConstants.CONNECTION_ALIVE_TIME * 1000;
|
|
43
|
+
RETRANSMIT_TIMEOUT = KNXnetIPEnum_1.KNXTimeoutConstants.TUNNELING_REQUEST_TIMEOUT * 1000;
|
|
44
|
+
MAX_PENDING_REQUESTS_PER_CLIENT = 100; // [MEJORA] Límite de ráfagas
|
|
45
|
+
maxTunnelConnections;
|
|
46
|
+
clientAddrsStartInt;
|
|
47
|
+
externalManager = null;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
super(options);
|
|
50
|
+
this._transport = "UDP";
|
|
51
|
+
// Set defaults for discovery if not provided
|
|
52
|
+
const routingOptions = this.options;
|
|
53
|
+
const netInfo = (0, localIp_1.getNetworkInfo)();
|
|
54
|
+
this.options.localIp = options.localIp || netInfo.address;
|
|
55
|
+
routingOptions.individualAddress = options.individualAddress || "15.15.0";
|
|
56
|
+
if (!KNXHelper_1.KNXHelper.isValidIndividualAddress(routingOptions.individualAddress)) {
|
|
57
|
+
throw new InvalidKnxAddresExeption_1.InvalidKnxAddressException(`This ${routingOptions.individualAddress} is not individual address`);
|
|
58
|
+
}
|
|
59
|
+
// Setup Logger
|
|
60
|
+
this.logger = this.logger.child({ module: this.constructor.name });
|
|
61
|
+
// Serial must be deterministic and unique per instance (MAC + Port), similar to knxd
|
|
62
|
+
if (!options.serialNumber) {
|
|
63
|
+
const macBuf = Buffer.from(netInfo.mac.replace(/[:\-]/g, ""), "hex");
|
|
64
|
+
const port = options.port || 3671;
|
|
65
|
+
const serial = Buffer.from(macBuf);
|
|
66
|
+
serial[0] ^= (port >> 8) & 0xff;
|
|
67
|
+
serial[1] ^= port & 0xff;
|
|
68
|
+
routingOptions.serialNumber = serial;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
routingOptions.serialNumber = options.serialNumber;
|
|
72
|
+
}
|
|
73
|
+
routingOptions.friendlyName = options.friendlyName || "KNX.ts Routing Node";
|
|
74
|
+
routingOptions.macAddress = options.macAddress || netInfo.mac;
|
|
75
|
+
routingOptions.routingDelay = options.routingDelay ?? 20;
|
|
76
|
+
if (routingOptions.MAX_PENDING_REQUESTS_PER_CLIENT)
|
|
77
|
+
this.MAX_PENDING_REQUESTS_PER_CLIENT = routingOptions.MAX_PENDING_REQUESTS_PER_CLIENT;
|
|
78
|
+
this.logger.info(`Initialized on ${this.options.localIp}:${options.port || 3671}`);
|
|
79
|
+
this.logger.info(`Serial Number: ${routingOptions.serialNumber.toString("hex").toUpperCase()}`);
|
|
80
|
+
const serverIA = KNXHelper_1.KNXHelper.GetAddress(routingOptions.individualAddress, ".").readUInt16BE();
|
|
81
|
+
this.serverIAInt = serverIA;
|
|
82
|
+
if (options.clientAddrs) {
|
|
83
|
+
const parts = options.clientAddrs.split(":");
|
|
84
|
+
if (parts.length === 2) {
|
|
85
|
+
this.clientAddrsStartInt = KNXHelper_1.KNXHelper.GetAddress(parts[0], ".").readUInt16BE();
|
|
86
|
+
this.maxTunnelConnections = parseInt(parts[1], 10);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.maxTunnelConnections = 15;
|
|
90
|
+
this.clientAddrsStartInt = serverIA + 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.maxTunnelConnections = 15;
|
|
95
|
+
this.clientAddrsStartInt = serverIA + 1;
|
|
96
|
+
}
|
|
97
|
+
if (options.externals) {
|
|
98
|
+
this.externalManager = new Router_1.Router(options.externals);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
get individualAddress() {
|
|
102
|
+
return KNXHelper_1.KNXHelper.GetAddress(this.serverIAInt, ".", true);
|
|
103
|
+
}
|
|
104
|
+
async connect() {
|
|
105
|
+
this.socket = dgram_1.default.createSocket({ type: "udp4", reuseAddr: true });
|
|
106
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
107
|
+
this.handleMessage(msg, rinfo);
|
|
108
|
+
});
|
|
109
|
+
this.socket.on("error", (err) => {
|
|
110
|
+
this.emit("error", err);
|
|
111
|
+
});
|
|
112
|
+
const connectPromise = new Promise((resolve, reject) => {
|
|
113
|
+
const socket = this.socket;
|
|
114
|
+
socket.bind(this.options.port, () => {
|
|
115
|
+
try {
|
|
116
|
+
socket.setBroadcast(true);
|
|
117
|
+
socket.setMulticastTTL(128);
|
|
118
|
+
socket.setMulticastLoopback(true);
|
|
119
|
+
// [MEJORA] Multi-homing: Unirse al multicast en todas las interfaces válidas (si está habilitado)
|
|
120
|
+
const interfaces = node_os_1.default.networkInterfaces();
|
|
121
|
+
const joinedInterfaces = new Set();
|
|
122
|
+
const useAllInterfaces = this.options.useAllInterfaces ?? true;
|
|
123
|
+
// Siempre intenta unirse primero a la localIp especificada
|
|
124
|
+
if (this.options.localIp && this.options.localIp !== "0.0.0.0") {
|
|
125
|
+
try {
|
|
126
|
+
socket.addMembership(this.options.ip, this.options.localIp);
|
|
127
|
+
joinedInterfaces.add(this.options.localIp);
|
|
128
|
+
this.logger.info(`Joined multicast on primary interface (${this.options.localIp})`);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
this.logger.debug(`Failed to join multicast on primary interface ${this.options.localIp}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (useAllInterfaces) {
|
|
135
|
+
// Itera sobre todas las demás interfaces de red del host
|
|
136
|
+
for (const name of Object.keys(interfaces)) {
|
|
137
|
+
for (const net of interfaces[name]) {
|
|
138
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
139
|
+
if (!joinedInterfaces.has(net.address)) {
|
|
140
|
+
try {
|
|
141
|
+
socket.addMembership(this.options.ip, net.address);
|
|
142
|
+
joinedInterfaces.add(net.address);
|
|
143
|
+
this.logger.info(`Joined multicast on interface ${name} (${net.address})`);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
// Ignora interfaces virtuales que no soportan IGMP
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
this.logger.info("Multi-homing disabled. Only primary interface used for multicast.");
|
|
155
|
+
}
|
|
156
|
+
// Central listener for all KNX indications (from IP Multicast, TP, or Tunnels)
|
|
157
|
+
this.on("indication", (cemi) => {
|
|
158
|
+
const body = cemi.toBuffer();
|
|
159
|
+
const srcIAStr = cemi.sourceAddress;
|
|
160
|
+
let busmonBody = null;
|
|
161
|
+
// Optional: Re-emit by Group Address for specific listening (e.g., server.on("1/1/1", (cemi) => ...))
|
|
162
|
+
if (cemi.controlField2 && cemi.controlField2.addressType === 1) {
|
|
163
|
+
this.emit(cemi.destinationAddress, cemi);
|
|
164
|
+
}
|
|
165
|
+
this._tunnelConnections.forEach((conn) => {
|
|
166
|
+
// Echo cancellation: Don't forward back to the client that originated this message
|
|
167
|
+
if (srcIAStr === conn.knxAddressStr) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
|
|
171
|
+
if (!busmonBody)
|
|
172
|
+
busmonBody = this.convertDataIndToBusmonInd(body);
|
|
173
|
+
conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Link Layer or Raw Layer
|
|
177
|
+
conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
this.emit("connected");
|
|
182
|
+
resolve();
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
this.emit("error", err);
|
|
186
|
+
reject(err);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
await connectPromise;
|
|
191
|
+
if (this.externalManager) {
|
|
192
|
+
this.externalManager.registerLink(this);
|
|
193
|
+
await this.externalManager.connect();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
disconnect() {
|
|
197
|
+
if (this.externalManager) {
|
|
198
|
+
this.externalManager.unregisterLink(this);
|
|
199
|
+
this.externalManager.disconnect();
|
|
200
|
+
}
|
|
201
|
+
if (this.socket) {
|
|
202
|
+
this.socket.close();
|
|
203
|
+
this.socket = null;
|
|
204
|
+
}
|
|
205
|
+
this.clearTimers();
|
|
206
|
+
}
|
|
207
|
+
// [MEJORA] Validación estricta Route Back (NAT Traversal) según Especificación 8.6.2.2
|
|
208
|
+
resolveRouteBack(hpai, rinfo) {
|
|
209
|
+
const isIpZero = hpai.ipAddress === "0.0.0.0";
|
|
210
|
+
const isPortZero = hpai.port === 0;
|
|
211
|
+
if (isIpZero && isPortZero) {
|
|
212
|
+
// Modo "Route Back" válido
|
|
213
|
+
hpai.ipAddress = rinfo.address;
|
|
214
|
+
hpai.port = rinfo.port;
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
else if (isIpZero || isPortZero) {
|
|
218
|
+
// Si solo UNO de los dos es 0, es un HPAI INVÁLIDO según la spec.
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
// Es un HPAI estándar, se queda tal cual
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
clearTimers() {
|
|
225
|
+
if (this.routingBusyTimer)
|
|
226
|
+
clearTimeout(this.routingBusyTimer);
|
|
227
|
+
if (this.decrementTimer)
|
|
228
|
+
clearTimeout(this.decrementTimer);
|
|
229
|
+
if (this.decrementInterval)
|
|
230
|
+
clearInterval(this.decrementInterval);
|
|
231
|
+
this.routingBusyTimer = null;
|
|
232
|
+
this.decrementTimer = null;
|
|
233
|
+
this.decrementInterval = null;
|
|
234
|
+
this._tunnelConnections.forEach((conn) => {
|
|
235
|
+
conn.close();
|
|
236
|
+
});
|
|
237
|
+
this._tunnelConnections.clear();
|
|
238
|
+
this.removeAllListeners("indication");
|
|
239
|
+
}
|
|
240
|
+
async send(data) {
|
|
241
|
+
let cemiBuffer;
|
|
242
|
+
let cemi;
|
|
243
|
+
if (Buffer.isBuffer(data)) {
|
|
244
|
+
cemiBuffer = data;
|
|
245
|
+
try {
|
|
246
|
+
cemi = CEMI_1.CEMI.fromBuffer(data);
|
|
247
|
+
}
|
|
248
|
+
catch (e) { }
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
cemi = data;
|
|
252
|
+
if (data.controlField2) {
|
|
253
|
+
const cf2 = data.controlField2;
|
|
254
|
+
const hopCount = cf2.hopCount;
|
|
255
|
+
if (hopCount === 0)
|
|
256
|
+
return;
|
|
257
|
+
if (hopCount < 7)
|
|
258
|
+
cf2.hopCount = hopCount - 1;
|
|
259
|
+
}
|
|
260
|
+
cemiBuffer = data.toBuffer();
|
|
261
|
+
}
|
|
262
|
+
if (cemi) {
|
|
263
|
+
this.emit("indication", cemi);
|
|
264
|
+
}
|
|
265
|
+
await this.enqueuePacket(cemiBuffer);
|
|
266
|
+
}
|
|
267
|
+
async sendRaw(cemiBuffer) {
|
|
268
|
+
try {
|
|
269
|
+
const cemi = CEMI_1.CEMI.fromBuffer(cemiBuffer);
|
|
270
|
+
this.emit("indication", cemi);
|
|
271
|
+
}
|
|
272
|
+
catch (e) { }
|
|
273
|
+
await this.enqueuePacket(cemiBuffer);
|
|
274
|
+
}
|
|
275
|
+
async enqueuePacket(cemiBuffer) {
|
|
276
|
+
if (this.msgQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
277
|
+
this.sendLostMessage(1);
|
|
278
|
+
this.emit("queue_overflow");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_INDICATION, 0);
|
|
282
|
+
header.totalLength = KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + cemiBuffer.length;
|
|
283
|
+
const packet = Buffer.concat([header.toBuffer(), cemiBuffer]);
|
|
284
|
+
this.msgQueue.push(packet);
|
|
285
|
+
if (this.msgQueue.length >= this.BUSY_THRESHOLD && !this.isRoutingBusy) {
|
|
286
|
+
const routingOptions = this.options;
|
|
287
|
+
const waitTime = (routingOptions.routingDelay ?? 20) * this.msgQueue.length;
|
|
288
|
+
this.sendRoutingBusy(Math.min(100, waitTime));
|
|
289
|
+
}
|
|
290
|
+
this.processQueue();
|
|
291
|
+
}
|
|
292
|
+
sendLostMessage(count) {
|
|
293
|
+
const lostMsg = new KNXnetIPStructures_1.RoutingLostMessage(0, count);
|
|
294
|
+
const msgBody = lostMsg.toBuffer();
|
|
295
|
+
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_LOST_MESSAGE, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + msgBody.length);
|
|
296
|
+
const packet = Buffer.concat([header.toBuffer(), msgBody]);
|
|
297
|
+
if (this.socket) {
|
|
298
|
+
this.socket.send(packet, this.options.port, this.options.ip, (err) => {
|
|
299
|
+
if (err)
|
|
300
|
+
this.emit("error", err);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
sendRoutingBusy(waitTime) {
|
|
305
|
+
const busyMsg = new KNXnetIPStructures_1.RoutingBusy(0, waitTime, 0x0000);
|
|
306
|
+
const msgBody = busyMsg.toBuffer();
|
|
307
|
+
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_BUSY, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + msgBody.length);
|
|
308
|
+
const packet = Buffer.concat([header.toBuffer(), msgBody]);
|
|
309
|
+
if (this.socket) {
|
|
310
|
+
this.socket.send(packet, this.options.port, this.options.ip, (err) => {
|
|
311
|
+
if (err)
|
|
312
|
+
this.emit("error", err);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
processQueue() {
|
|
317
|
+
if (this.isProcessingQueue || this.isRoutingBusy || this.msgQueue.length === 0)
|
|
318
|
+
return;
|
|
319
|
+
this.isProcessingQueue = true;
|
|
320
|
+
const routingOptions = this.options;
|
|
321
|
+
const delay = routingOptions.routingDelay ?? 20;
|
|
322
|
+
const executeSend = () => {
|
|
323
|
+
if (!this.socket || this.isRoutingBusy) {
|
|
324
|
+
this.isProcessingQueue = false;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const packet = this.msgQueue.shift();
|
|
328
|
+
if (packet) {
|
|
329
|
+
const startTime = Date.now();
|
|
330
|
+
this.socket.send(packet, this.options.port, this.options.ip, (err) => {
|
|
331
|
+
if (err)
|
|
332
|
+
this.emit("error", err);
|
|
333
|
+
this.lastSentTime = Date.now();
|
|
334
|
+
this.isProcessingQueue = false;
|
|
335
|
+
if (this.msgQueue.length > 0) {
|
|
336
|
+
const elapsed = this.lastSentTime - startTime;
|
|
337
|
+
const nextWait = Math.max(0, delay - elapsed);
|
|
338
|
+
if (nextWait === 0)
|
|
339
|
+
setImmediate(() => this.processQueue());
|
|
340
|
+
else
|
|
341
|
+
setTimeout(() => this.processQueue(), nextWait);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
this.isProcessingQueue = false;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
const initialWait = Math.max(0, delay - (now - this.lastSentTime));
|
|
351
|
+
if (initialWait === 0)
|
|
352
|
+
executeSend();
|
|
353
|
+
else
|
|
354
|
+
setTimeout(executeSend, initialWait);
|
|
355
|
+
}
|
|
356
|
+
handleMessage(msg, rinfo) {
|
|
357
|
+
try {
|
|
358
|
+
const header = KNXnetIPHeader_1.KNXnetIPHeader.fromBuffer(msg);
|
|
359
|
+
const body = msg.subarray(6);
|
|
360
|
+
const ourAddress = this.socket.address();
|
|
361
|
+
// Filtro Anti-Eco inicial por IP/Puerto
|
|
362
|
+
if (rinfo.address === this.options.localIp && rinfo.port === ourAddress.port)
|
|
363
|
+
return;
|
|
364
|
+
switch (header.serviceType) {
|
|
365
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_INDICATION:
|
|
366
|
+
// [MEJORA] Filtro Anti-Eco Seguro leyendo la Individual Address (IA) origen del CEMI
|
|
367
|
+
const addInfoLen = body[1];
|
|
368
|
+
if (body.length >= 6 + addInfoLen) {
|
|
369
|
+
const srcIA = body.readUInt16BE(4 + addInfoLen);
|
|
370
|
+
if (srcIA === this.serverIAInt) {
|
|
371
|
+
return; // Es nuestro propio paquete Multicast reenviado por el router
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
this.emit("raw_indication", body);
|
|
375
|
+
try {
|
|
376
|
+
const cemi = CEMI_1.CEMI.fromBuffer(body);
|
|
377
|
+
this.emit("indication", cemi);
|
|
378
|
+
}
|
|
379
|
+
catch (e) { }
|
|
380
|
+
break;
|
|
381
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_BUSY:
|
|
382
|
+
this.handleRoutingBusy(KNXnetIPStructures_1.RoutingBusy.fromBuffer(body));
|
|
383
|
+
break;
|
|
384
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_LOST_MESSAGE:
|
|
385
|
+
this.emit("routing_lost_message", KNXnetIPStructures_1.RoutingLostMessage.fromBuffer(body));
|
|
386
|
+
break;
|
|
387
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_REQUEST:
|
|
388
|
+
this.handleSearchRequest(msg, rinfo, false);
|
|
389
|
+
break;
|
|
390
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_REQUEST_EXTENDED:
|
|
391
|
+
this.handleSearchRequest(msg, rinfo, true);
|
|
392
|
+
break;
|
|
393
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.DESCRIPTION_REQUEST:
|
|
394
|
+
this.handleDescriptionRequest(msg, rinfo);
|
|
395
|
+
break;
|
|
396
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_REQUEST:
|
|
397
|
+
this.handleConnectRequest(msg, rinfo);
|
|
398
|
+
break;
|
|
399
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_REQUEST:
|
|
400
|
+
this.handleConnectionStateRequest(msg, rinfo);
|
|
401
|
+
break;
|
|
402
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.DISCONNECT_REQUEST:
|
|
403
|
+
this.handleDisconnectRequest(msg, rinfo);
|
|
404
|
+
break;
|
|
405
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST:
|
|
406
|
+
this.handleTunnelingRequest(msg, rinfo);
|
|
407
|
+
break;
|
|
408
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_ACK:
|
|
409
|
+
this.handleTunnelingAck(msg);
|
|
410
|
+
break;
|
|
411
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_ACK:
|
|
412
|
+
this.handleDeviceConfigAck(msg);
|
|
413
|
+
break;
|
|
414
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_GET:
|
|
415
|
+
this.handleTunnelingFeatureGet(msg);
|
|
416
|
+
break;
|
|
417
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_REQUEST:
|
|
418
|
+
this.handleDeviceConfigurationRequest(msg);
|
|
419
|
+
break;
|
|
420
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_SYSTEM_BROADCAST:
|
|
421
|
+
this.emit("routing_system_broadcast", body);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
this.emit("error", e);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
handleTunnelingAck(msg) {
|
|
430
|
+
const channelId = msg.readUInt8(7);
|
|
431
|
+
const seq = msg.readUInt8(8);
|
|
432
|
+
const status = msg.readUInt8(9);
|
|
433
|
+
this.logger.debug(`Received Tunnelling ACK for channel ${channelId}, seq ${seq}, status ${status}`);
|
|
434
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
435
|
+
if (conn) {
|
|
436
|
+
conn.handleAck(seq, status);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
handleSearchRequest(msg, rinfo, isExtended) {
|
|
440
|
+
const clientHPAI = KNXnetIPStructures_1.HPAI.fromBuffer(msg.subarray(6));
|
|
441
|
+
// [MEJORA] Route Back validation
|
|
442
|
+
if (!this.resolveRouteBack(clientHPAI, rinfo)) {
|
|
443
|
+
return; // Silently drop invalid HPAI
|
|
444
|
+
}
|
|
445
|
+
const responseType = isExtended ? KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE_EXTENDED : KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE;
|
|
446
|
+
const serverHPAI = this.getHPAI(rinfo);
|
|
447
|
+
const localIp = serverHPAI.ipAddress;
|
|
448
|
+
const localPort = serverHPAI.port;
|
|
449
|
+
this.logger.debug(`Responding to search from ${clientHPAI.ipAddress}:${clientHPAI.port} with ${localIp}:${localPort}`);
|
|
450
|
+
const dibs = this.getIdentificationDIBs(responseType, localIp);
|
|
451
|
+
const body = Buffer.concat([serverHPAI.toBuffer(), ...dibs.map((d) => d.toBuffer())]);
|
|
452
|
+
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(responseType, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
453
|
+
if (this.socket) {
|
|
454
|
+
this.socket.send(Buffer.concat([responseHeader.toBuffer(), body]), clientHPAI.port, clientHPAI.ipAddress);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
handleDescriptionRequest(msg, rinfo) {
|
|
458
|
+
const clientHPAI = KNXnetIPStructures_1.HPAI.fromBuffer(msg.subarray(6));
|
|
459
|
+
// [MEJORA] Route Back validation
|
|
460
|
+
if (!this.resolveRouteBack(clientHPAI, rinfo))
|
|
461
|
+
return;
|
|
462
|
+
const serverHPAI = this.getHPAI(rinfo);
|
|
463
|
+
const dibs = this.getIdentificationDIBs(KNXnetIPEnum_1.KNXnetIPServiceType.DESCRIPTION_RESPONSE, serverHPAI.ipAddress);
|
|
464
|
+
const body = Buffer.concat(dibs.map((d) => d.toBuffer()));
|
|
465
|
+
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.DESCRIPTION_RESPONSE, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
466
|
+
this.logger.debug(`Responding to description request from ${clientHPAI.ipAddress}:${clientHPAI.port}`);
|
|
467
|
+
if (this.socket) {
|
|
468
|
+
this.socket.send(Buffer.concat([responseHeader.toBuffer(), body]), clientHPAI.port, clientHPAI.ipAddress);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
getHPAI(rinfo) {
|
|
472
|
+
let localIp = this.options.localIp;
|
|
473
|
+
if (localIp === "0.0.0.0") {
|
|
474
|
+
localIp = (0, localIp_1.getNetworkInfo)().address;
|
|
475
|
+
}
|
|
476
|
+
// [MEJORA] Multi-homed IP matching.
|
|
477
|
+
// Ensure we report the local IP of the interface that can actually route back to the client.
|
|
478
|
+
if (rinfo && rinfo.address) {
|
|
479
|
+
const interfaces = node_os_1.default.networkInterfaces();
|
|
480
|
+
const rinfoNum = rinfo.address.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
481
|
+
for (const name of Object.keys(interfaces)) {
|
|
482
|
+
for (const net of interfaces[name]) {
|
|
483
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
484
|
+
const netNum = net.address.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
485
|
+
const maskNum = net.netmask.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
486
|
+
if ((rinfoNum & maskNum) === (netNum & maskNum)) {
|
|
487
|
+
localIp = net.address;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return new KNXnetIPStructures_1.HPAI(KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, localIp, this.socket.address().port);
|
|
494
|
+
}
|
|
495
|
+
handleConnectRequest(msg, rinfo) {
|
|
496
|
+
const clientControlHPAI = KNXnetIPStructures_1.HPAI.fromBuffer(msg.subarray(6));
|
|
497
|
+
const clientDataHPAI = KNXnetIPStructures_1.HPAI.fromBuffer(msg.subarray(14));
|
|
498
|
+
// [MEJORA] Route Back validation estricta para el control y el data endpoint
|
|
499
|
+
if (!this.resolveRouteBack(clientControlHPAI, rinfo)) {
|
|
500
|
+
this.logger.warn(`Invalid Control HPAI from ${rinfo.address}. Dropping.`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (!this.resolveRouteBack(clientDataHPAI, rinfo)) {
|
|
504
|
+
this.logger.warn(`Invalid Data HPAI from ${rinfo.address}. Dropping.`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const cri = KNXnetIPStructures_1.CRI.fromBuffer(msg.subarray(22));
|
|
508
|
+
let status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR;
|
|
509
|
+
let channelId = 0;
|
|
510
|
+
const serverDataHPAI = this.getHPAI(rinfo);
|
|
511
|
+
this.logger.info(`Connect Request from IP: ${rinfo.address}, Type: ${cri.connectionType}, IA: ${cri.individualAddress}, Layer: ${cri.knxLayer}`);
|
|
512
|
+
// Check if a connection from the same IP already exists with the same IA
|
|
513
|
+
// If so, it might be a stale connection from a client that crashed/restarted
|
|
514
|
+
let knxAddress = cri.individualAddress;
|
|
515
|
+
if (knxAddress !== null && knxAddress !== 0) {
|
|
516
|
+
for (const [cid, conn] of this._tunnelConnections.entries()) {
|
|
517
|
+
if (conn.knxAddress === knxAddress && conn.controlHPAI.ipAddress === rinfo.address) {
|
|
518
|
+
this.logger.warn(`IA ${knxAddress} already in use by stale connection from same IP ${rinfo.address}. Replacing channel ${cid}.`);
|
|
519
|
+
this.closeConnection(cid, true);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (let i = 1; i <= this.maxTunnelConnections; i++) {
|
|
525
|
+
if (!this._tunnelConnections.has(i)) {
|
|
526
|
+
channelId = i;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (channelId === 0) {
|
|
531
|
+
this.logger.warn("Connect Request refused: No more channels available.");
|
|
532
|
+
status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_MORE_CONNECTIONS;
|
|
533
|
+
}
|
|
534
|
+
else if (cri.connectionType === KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION) {
|
|
535
|
+
// Management connections don't usually have a dedicated IA assigned in the CRD
|
|
536
|
+
this.logger.info(`Management Connection established! Channel: ${channelId}`);
|
|
537
|
+
this._tunnelConnections.set(channelId, new TunnelConnection_1.TunnelConnection(channelId, clientControlHPAI, clientDataHPAI, 0, "0.0.0", cri.knxLayer, this.socket, this.HEARTBEAT_TIMEOUT, this.RETRANSMIT_TIMEOUT, this.MAX_PENDING_REQUESTS_PER_CLIENT, (cid, sendDisconnect) => this.closeConnection(cid, sendDisconnect), this.logger));
|
|
538
|
+
const body = Buffer.concat([
|
|
539
|
+
Buffer.from([channelId, status]),
|
|
540
|
+
serverDataHPAI.toBuffer(),
|
|
541
|
+
Buffer.from([0x02, KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION])
|
|
542
|
+
]);
|
|
543
|
+
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
544
|
+
if (this.socket)
|
|
545
|
+
this.socket.send(Buffer.concat([responseHeader.toBuffer(), body]), clientControlHPAI.port, clientControlHPAI.ipAddress);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
else if (cri.connectionType === KNXnetIPEnum_1.ConnectionType.TUNNEL_CONNECTION) {
|
|
549
|
+
if (knxAddress === null || knxAddress === 0) {
|
|
550
|
+
knxAddress = this.clientAddrsStartInt + channelId - 1;
|
|
551
|
+
}
|
|
552
|
+
if (cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.LINK_LAYER && cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER && cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.RAW_LAYER) {
|
|
553
|
+
this.logger.warn(`Connect Request refused: Invalid layer ${cri.knxLayer}`);
|
|
554
|
+
status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_TUNNELLING_LAYER;
|
|
555
|
+
}
|
|
556
|
+
if (status === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
|
|
557
|
+
for (const [cid, conn] of this._tunnelConnections.entries()) {
|
|
558
|
+
if (conn.knxAddress === knxAddress) {
|
|
559
|
+
this.logger.warn(`Connect Request refused: IA ${knxAddress} already in use by channel ${cid}`);
|
|
560
|
+
status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_MORE_UNIQUE_CONNECTIONS;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (status === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
|
|
566
|
+
const addrBuf = Buffer.alloc(2);
|
|
567
|
+
addrBuf.writeUInt16BE(knxAddress);
|
|
568
|
+
const knxAddressStr = KNXHelper_1.KNXHelper.GetAddress(addrBuf, ".");
|
|
569
|
+
this.logger.info(`Tunnel Connection established! Channel: ${channelId}, IA: ${knxAddressStr}, Layer: ${cri.knxLayer}`);
|
|
570
|
+
this._tunnelConnections.set(channelId, new TunnelConnection_1.TunnelConnection(channelId, clientControlHPAI, clientDataHPAI, knxAddress, knxAddressStr, cri.knxLayer, this.socket, this.HEARTBEAT_TIMEOUT, this.RETRANSMIT_TIMEOUT, this.MAX_PENDING_REQUESTS_PER_CLIENT, (cid, sendDisconnect) => this.closeConnection(cid, sendDisconnect), this.logger));
|
|
571
|
+
const crd = new KNXnetIPStructures_1.CRD(cri.connectionType, knxAddress);
|
|
572
|
+
const body = Buffer.concat([Buffer.from([channelId, status]), serverDataHPAI.toBuffer(), crd.toBuffer()]);
|
|
573
|
+
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
574
|
+
if (this.socket)
|
|
575
|
+
this.socket.send(Buffer.concat([responseHeader.toBuffer(), body]), clientControlHPAI.port, clientControlHPAI.ipAddress);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_CONNECTION_TYPE;
|
|
581
|
+
}
|
|
582
|
+
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE, 8);
|
|
583
|
+
if (this.socket)
|
|
584
|
+
this.socket.send(Buffer.concat([responseHeader.toBuffer(), Buffer.from([0, status])]), clientControlHPAI.port, clientControlHPAI.ipAddress);
|
|
585
|
+
}
|
|
586
|
+
resetHeartbeat(channelId) {
|
|
587
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
588
|
+
if (conn) {
|
|
589
|
+
conn.resetHeartbeat();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
closeConnection(channelId, sendDisconnect = false) {
|
|
593
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
594
|
+
if (conn) {
|
|
595
|
+
const controlHPAI = conn.controlHPAI;
|
|
596
|
+
conn.close();
|
|
597
|
+
if (sendDisconnect && this.socket) {
|
|
598
|
+
// Send DISCONNECT_REQUEST to client (Spec 5.4/5.5)
|
|
599
|
+
const body = Buffer.concat([Buffer.from([channelId, 0x00]), this.getHPAI().toBuffer()]);
|
|
600
|
+
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.DISCONNECT_REQUEST, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
601
|
+
this.socket.send(Buffer.concat([header.toBuffer(), body]), controlHPAI.port, controlHPAI.ipAddress);
|
|
602
|
+
}
|
|
603
|
+
this._tunnelConnections.delete(channelId);
|
|
604
|
+
this.emit("disconnected", channelId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
handleConnectionStateRequest(msg, rinfo) {
|
|
608
|
+
const channelId = msg.readUInt8(6);
|
|
609
|
+
const clientControlHPAI = KNXnetIPStructures_1.HPAI.fromBuffer(msg.subarray(8));
|
|
610
|
+
// [MEJORA] Route Back validation
|
|
611
|
+
if (!this.resolveRouteBack(clientControlHPAI, rinfo))
|
|
612
|
+
return;
|
|
613
|
+
let status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_CONNECTION_ID;
|
|
614
|
+
if (this._tunnelConnections.has(channelId)) {
|
|
615
|
+
status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR;
|
|
616
|
+
this.resetHeartbeat(channelId);
|
|
617
|
+
}
|
|
618
|
+
const body = Buffer.from([channelId, status]);
|
|
619
|
+
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_RESPONSE, 6 + body.length);
|
|
620
|
+
if (this.socket)
|
|
621
|
+
this.socket.send(Buffer.concat([responseHeader.toBuffer(), body]), clientControlHPAI.port, clientControlHPAI.ipAddress);
|
|
622
|
+
}
|
|
623
|
+
handleDisconnectRequest(msg, rinfo) {
|
|
624
|
+
const channelId = msg.readUInt8(6);
|
|
625
|
+
const clientControlHPAI = KNXnetIPStructures_1.HPAI.fromBuffer(msg.subarray(8));
|
|
626
|
+
// [MEJORA] Route Back validation
|
|
627
|
+
if (!this.resolveRouteBack(clientControlHPAI, rinfo))
|
|
628
|
+
return;
|
|
629
|
+
this.closeConnection(channelId, false);
|
|
630
|
+
const body = Buffer.from([channelId, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR]);
|
|
631
|
+
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.DISCONNECT_RESPONSE, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
632
|
+
if (this.socket)
|
|
633
|
+
this.socket.send(Buffer.concat([responseHeader.toBuffer(), body]), clientControlHPAI.port, clientControlHPAI.ipAddress);
|
|
634
|
+
}
|
|
635
|
+
handleTunnelingRequest(msg, rinfo) {
|
|
636
|
+
const headerLen = msg.readUInt8(6);
|
|
637
|
+
const channelId = msg.readUInt8(7);
|
|
638
|
+
const seq = msg.readUInt8(8);
|
|
639
|
+
const cemiBuffer = msg.subarray(6 + headerLen);
|
|
640
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
641
|
+
if (!conn) {
|
|
642
|
+
this.sendTunnelACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_CONNECTION_ID, rinfo);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// [MEJORA] Rate Limiting (Pacing) para evitar Flooding por parte del cliente
|
|
646
|
+
const now = Date.now();
|
|
647
|
+
if (now - conn.lastRxTime > 1000) {
|
|
648
|
+
conn.rxCount = 0;
|
|
649
|
+
conn.lastRxTime = now;
|
|
650
|
+
}
|
|
651
|
+
conn.rxCount++;
|
|
652
|
+
if (this.MAX_PENDING_REQUESTS_PER_CLIENT > 0 && conn.rxCount > this.MAX_PENDING_REQUESTS_PER_CLIENT) {
|
|
653
|
+
this.logger.warn(`Client ${rinfo.address} is flooding (${conn.rxCount} req/s). Terminating connection on channel ${channelId}.`);
|
|
654
|
+
this.closeConnection(channelId, true);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const { action, status } = conn.validateRequest(seq);
|
|
658
|
+
if (action === 'retransmit_ack') {
|
|
659
|
+
this.sendTunnelACK(channelId, seq, status, rinfo);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (action === 'discard')
|
|
663
|
+
return;
|
|
664
|
+
this.sendTunnelACK(channelId, seq, status, rinfo);
|
|
665
|
+
const msgCode = cemiBuffer[0];
|
|
666
|
+
const addInfoLen = cemiBuffer[1];
|
|
667
|
+
if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER && (msgCode === 0x11 || msgCode === 0x10))
|
|
668
|
+
return;
|
|
669
|
+
if (msgCode === 0x11) {
|
|
670
|
+
const srcIAOffset = 2 + addInfoLen + 2;
|
|
671
|
+
if (cemiBuffer.readUInt16BE(srcIAOffset) === 0) {
|
|
672
|
+
cemiBuffer.writeUInt16BE(conn.knxAddress, srcIAOffset);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
let routingCemiBuffer = cemiBuffer;
|
|
676
|
+
if (msgCode === 0x11) {
|
|
677
|
+
routingCemiBuffer = Buffer.from(cemiBuffer);
|
|
678
|
+
routingCemiBuffer[0] = 0x29;
|
|
679
|
+
}
|
|
680
|
+
else if (msgCode === 0x10) {
|
|
681
|
+
routingCemiBuffer = Buffer.from(cemiBuffer);
|
|
682
|
+
routingCemiBuffer[0] = 0x2d;
|
|
683
|
+
}
|
|
684
|
+
this.sendRaw(routingCemiBuffer);
|
|
685
|
+
if (msgCode === 0x11 || msgCode === 0x10) {
|
|
686
|
+
const conCemiBuffer = Buffer.from(cemiBuffer);
|
|
687
|
+
conCemiBuffer[0] = msgCode + 0x1d; // 0x11 -> 0x2E, 0x10 -> 0x2F
|
|
688
|
+
conCemiBuffer[2 + addInfoLen] &= 0xfe;
|
|
689
|
+
conn.enqueue(conCemiBuffer, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
sendTunnelACK(channelId, seq, status, rinfo) {
|
|
693
|
+
const body = Buffer.from([0x04, channelId, seq, status]);
|
|
694
|
+
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_ACK, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
695
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
696
|
+
if (this.socket) {
|
|
697
|
+
const port = conn ? conn.dataHPAI.port : (rinfo ? rinfo.port : 0);
|
|
698
|
+
const addr = conn ? conn.dataHPAI.ipAddress : (rinfo ? rinfo.address : "");
|
|
699
|
+
if (port > 0 && addr !== "") {
|
|
700
|
+
this.socket.send(Buffer.concat([header.toBuffer(), body]), port, addr);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
handleTunnelingFeatureGet(msg) {
|
|
705
|
+
const channelId = msg.readUInt8(7);
|
|
706
|
+
const seq = msg.readUInt8(8);
|
|
707
|
+
const featId = msg.readUInt8(10);
|
|
708
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
709
|
+
if (!conn) {
|
|
710
|
+
this.sendTunnelACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_CONNECTION_ID);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const { action, status } = conn.validateRequest(seq);
|
|
714
|
+
this.logger.debug(`Feature Get for channel ${channelId}, feat: ${featId}, seq: ${seq}`);
|
|
715
|
+
if (action === 'retransmit_ack') {
|
|
716
|
+
this.sendTunnelACK(channelId, seq, status);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (action === 'discard')
|
|
720
|
+
return;
|
|
721
|
+
this.sendTunnelACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
|
|
722
|
+
let featVal;
|
|
723
|
+
let retCode = 0x00;
|
|
724
|
+
switch (featId) {
|
|
725
|
+
case 0x07:
|
|
726
|
+
featVal = Buffer.alloc(2);
|
|
727
|
+
featVal.writeUInt16BE(254);
|
|
728
|
+
break;
|
|
729
|
+
case 0x06:
|
|
730
|
+
featVal = Buffer.alloc(2);
|
|
731
|
+
featVal.writeUInt16BE(conn.knxAddress);
|
|
732
|
+
break;
|
|
733
|
+
default:
|
|
734
|
+
featVal = Buffer.alloc(0);
|
|
735
|
+
retCode = 0x01;
|
|
736
|
+
}
|
|
737
|
+
const featBody = Buffer.concat([Buffer.from([featId, retCode]), featVal]);
|
|
738
|
+
conn.enqueue(featBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_RESPONSE);
|
|
739
|
+
}
|
|
740
|
+
handleDeviceConfigurationRequest(msg) {
|
|
741
|
+
const headerLen = msg.readUInt8(6);
|
|
742
|
+
const channelId = msg.readUInt8(7);
|
|
743
|
+
const seq = msg.readUInt8(8);
|
|
744
|
+
const cemiBuffer = msg.subarray(6 + headerLen);
|
|
745
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
746
|
+
if (!conn) {
|
|
747
|
+
this.sendDeviceConfigACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_CONNECTION_ID);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const { action, status } = conn.validateRequest(seq);
|
|
751
|
+
if (action === 'retransmit_ack') {
|
|
752
|
+
this.sendDeviceConfigACK(channelId, seq, status);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (action === 'discard')
|
|
756
|
+
return;
|
|
757
|
+
this.sendDeviceConfigACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
|
|
758
|
+
try {
|
|
759
|
+
const msgCode = cemiBuffer.readUInt8(0);
|
|
760
|
+
if (msgCode === 0xFC) { // M_PropRead.req
|
|
761
|
+
const req = CEMI_1.CEMI.ManagementCEMI["M_PropRead.req"].fromBuffer(cemiBuffer);
|
|
762
|
+
this.logger.debug(`Management PropRead: Obj=${req.interfaceObjectType}, Prop=${req.propertyId} on channel ${channelId}`);
|
|
763
|
+
let data = Buffer.alloc(0);
|
|
764
|
+
const routingOptions = this.options;
|
|
765
|
+
// Respond to Individual Address (Prop 1) of Device Object (Obj 0)
|
|
766
|
+
if (req.interfaceObjectType === 0 && req.propertyId === 1) {
|
|
767
|
+
data = Buffer.alloc(2);
|
|
768
|
+
data.writeUInt16BE(KNXHelper_1.KNXHelper.GetAddress(routingOptions.individualAddress, ".").readUInt16BE());
|
|
769
|
+
}
|
|
770
|
+
const resCemi = new CEMI_1.CEMI.ManagementCEMI["M_PropRead.con"](req.interfaceObjectType, req.objectInstance, req.propertyId, req.numberOfElements, req.startIndex, data);
|
|
771
|
+
conn.enqueue(resCemi.toBuffer(), KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_REQUEST);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
catch (e) {
|
|
775
|
+
this.logger.error(`Error processing management config request on channel ${channelId}: ${e.message}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
handleDeviceConfigAck(msg) {
|
|
779
|
+
const channelId = msg.readUInt8(7);
|
|
780
|
+
const seq = msg.readUInt8(8);
|
|
781
|
+
const status = msg.readUInt8(9);
|
|
782
|
+
this.logger.debug(`Received Management ACK for channel ${channelId}, seq ${seq}, status ${status}`);
|
|
783
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
784
|
+
if (conn) {
|
|
785
|
+
conn.handleAck(seq, status);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
sendDeviceConfigACK(channelId, seq, status) {
|
|
789
|
+
const body = Buffer.from([0x04, channelId, seq, status]);
|
|
790
|
+
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_ACK, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
791
|
+
const conn = this._tunnelConnections.get(channelId);
|
|
792
|
+
if (conn && this.socket) {
|
|
793
|
+
this.socket.send(Buffer.concat([header.toBuffer(), body]), conn.dataHPAI.port, conn.dataHPAI.ipAddress);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
getIdentificationDIBs(serviceType, requestLocalIp) {
|
|
797
|
+
const routingOptions = this.options;
|
|
798
|
+
const netInfo = (0, localIp_1.getNetworkInfo)();
|
|
799
|
+
const effectiveLocalIp = requestLocalIp || netInfo.address;
|
|
800
|
+
let effectiveNetmask = netInfo.netmask;
|
|
801
|
+
if (requestLocalIp && requestLocalIp !== netInfo.address) {
|
|
802
|
+
const interfaces = node_os_1.default.networkInterfaces();
|
|
803
|
+
for (const name of Object.keys(interfaces)) {
|
|
804
|
+
for (const net of interfaces[name]) {
|
|
805
|
+
if (net.address === requestLocalIp) {
|
|
806
|
+
effectiveNetmask = net.netmask;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// Spec says use TP1 (0x02) for gateway reporting
|
|
812
|
+
const devInfo = new KNXnetIPStructures_1.DeviceInformationDIB(32 /* KNXMedium.KNXIP */, 0, KNXHelper_1.KNXHelper.GetAddress(routingOptions.individualAddress, ".").readUint16BE(), 0, routingOptions.serialNumber, this.options.ip, routingOptions.macAddress, routingOptions.friendlyName);
|
|
813
|
+
const suppSvc = new KNXnetIPStructures_1.SupportedServicesDIB([
|
|
814
|
+
{ family: 2 /* AllowedSupportedServiceFamilies.Core */, version: 1 },
|
|
815
|
+
{ family: 3 /* AllowedSupportedServiceFamilies.DeviceManagement */, version: 1 },
|
|
816
|
+
{ family: 4 /* AllowedSupportedServiceFamilies.Tunnelling */, version: 1 },
|
|
817
|
+
{ family: 5 /* AllowedSupportedServiceFamilies.Routing */, version: 1 },
|
|
818
|
+
]);
|
|
819
|
+
if (serviceType === KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE) {
|
|
820
|
+
return [devInfo, suppSvc];
|
|
821
|
+
}
|
|
822
|
+
const deviceDescriptorType0 = DeviceDescriptorType_1.DeviceDescriptorType0.KNXNET_IP_ROUTER;
|
|
823
|
+
const extDevInfo = new KNXnetIPStructures_1.ExtendedDeviceInformationDIB(false, 254, deviceDescriptorType0);
|
|
824
|
+
const ipConfig = new KNXnetIPStructures_1.IPConfigDIB(effectiveLocalIp, effectiveNetmask, "0.0.0.0", 0x01, 0x02);
|
|
825
|
+
const ipCurrent = new KNXnetIPStructures_1.IPCurrentConfigDIB(effectiveLocalIp, effectiveNetmask, "0.0.0.0", "0.0.0.0", 0x02);
|
|
826
|
+
const slots = [];
|
|
827
|
+
for (let i = 1; i <= this.maxTunnelConnections; i++) {
|
|
828
|
+
const conn = this._tunnelConnections.get(i);
|
|
829
|
+
const status = new KNXnetIPStructures_1.StatusTunnelingSlot();
|
|
830
|
+
status.authorised = true;
|
|
831
|
+
status.usable = !!conn;
|
|
832
|
+
status.free = !conn;
|
|
833
|
+
slots.push({
|
|
834
|
+
address: conn ? conn.knxAddress : this.clientAddrsStartInt + i - 1,
|
|
835
|
+
status: status
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return [devInfo, suppSvc, extDevInfo, ipConfig, ipCurrent, new KNXnetIPStructures_1.TunnellingInfoDIB(254, slots)];
|
|
839
|
+
}
|
|
840
|
+
handleRoutingBusy(busy) {
|
|
841
|
+
const now = Date.now();
|
|
842
|
+
if (now - this.lastBusyTime > 10) {
|
|
843
|
+
this.busyCounter++;
|
|
844
|
+
this.resetDecrementTimer();
|
|
845
|
+
}
|
|
846
|
+
this.lastBusyTime = now;
|
|
847
|
+
if (busy.routingBusyControl === 0x0000) {
|
|
848
|
+
this.pauseSending(busy.waitTime + Math.floor(Math.random() * this.busyCounter * 50));
|
|
849
|
+
}
|
|
850
|
+
this.emit("routing_busy", busy);
|
|
851
|
+
}
|
|
852
|
+
resetDecrementTimer() {
|
|
853
|
+
if (this.decrementTimer)
|
|
854
|
+
clearTimeout(this.decrementTimer);
|
|
855
|
+
if (this.decrementInterval)
|
|
856
|
+
clearInterval(this.decrementInterval);
|
|
857
|
+
this.decrementTimer = setTimeout(() => {
|
|
858
|
+
this.decrementInterval = setInterval(() => {
|
|
859
|
+
if (this.busyCounter > 0)
|
|
860
|
+
this.busyCounter--;
|
|
861
|
+
else {
|
|
862
|
+
if (this.decrementInterval)
|
|
863
|
+
clearInterval(this.decrementInterval);
|
|
864
|
+
this.decrementInterval = null;
|
|
865
|
+
}
|
|
866
|
+
}, 5);
|
|
867
|
+
}, this.busyCounter * 100);
|
|
868
|
+
}
|
|
869
|
+
pauseSending(waitTime) {
|
|
870
|
+
this.isRoutingBusy = true;
|
|
871
|
+
if (this.routingBusyTimer)
|
|
872
|
+
clearTimeout(this.routingBusyTimer);
|
|
873
|
+
this.routingBusyTimer = setTimeout(() => {
|
|
874
|
+
this.isRoutingBusy = false;
|
|
875
|
+
this.routingBusyTimer = null;
|
|
876
|
+
this.emit("routing_ready");
|
|
877
|
+
this.processQueue();
|
|
878
|
+
}, waitTime);
|
|
879
|
+
}
|
|
880
|
+
convertDataIndToBusmonInd(cemiBuffer) {
|
|
881
|
+
const msgCode = cemiBuffer[0];
|
|
882
|
+
if (msgCode !== 0x29 && msgCode !== 0x2d && msgCode !== 0x2e && msgCode !== 0x11 && msgCode !== 0x10)
|
|
883
|
+
return cemiBuffer;
|
|
884
|
+
const addInfoLen = cemiBuffer[1];
|
|
885
|
+
const baseOffset = 2 + addInfoLen;
|
|
886
|
+
const cf1 = cemiBuffer[baseOffset];
|
|
887
|
+
const cf2 = cemiBuffer[baseOffset + 1];
|
|
888
|
+
const src = cemiBuffer.subarray(baseOffset + 2, baseOffset + 4);
|
|
889
|
+
const dst = cemiBuffer.subarray(baseOffset + 4, baseOffset + 6);
|
|
890
|
+
const dataLen = cemiBuffer[baseOffset + 6];
|
|
891
|
+
const tpdu = cemiBuffer.subarray(baseOffset + 7);
|
|
892
|
+
const lpdu = Buffer.concat([Buffer.from([cf1]), src, dst, Buffer.from([(cf2 & 0xf0) | (dataLen + 1)]), tpdu, Buffer.alloc(1)]);
|
|
893
|
+
let xor = 0;
|
|
894
|
+
for (let i = 0; i < lpdu.length - 1; i++)
|
|
895
|
+
xor ^= lpdu[i];
|
|
896
|
+
lpdu[lpdu.length - 1] = ~xor & 0xff;
|
|
897
|
+
return new CEMI_1.CEMI.DataLinkLayerCEMI["L_Busmon.ind"](null, lpdu).toBuffer();
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
exports.KNXnetIPServer = KNXnetIPServer;
|