Skip to content

Commit a57d8a2

Browse files
committed
Add mediation server
1 parent de4bcab commit a57d8a2

File tree

9 files changed

+1986
-0
lines changed

9 files changed

+1986
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
const { Buffer } = require('buffer');
2+
const { NATTypes, MessageTypes, StatusTypes, Config } = require('./constants');
3+
4+
class ConnectionManager {
5+
constructor() {
6+
this.sockets = [];
7+
this.udpConnectionInfo = [];
8+
this.currentConnectionPairs = {};
9+
this.connectionId = 1;
10+
}
11+
12+
addSocket(socket, remoteAddress, remotePort, timeout) {
13+
const socketInfo = {
14+
socket,
15+
ip: remoteAddress,
16+
tcpPort: remotePort,
17+
timeout,
18+
natType: NATTypes.Unknown,
19+
localPort: 0,
20+
externalPortOne: 0,
21+
externalPortTwo: 0,
22+
clientID: Math.random().toString(),
23+
isServer: false // Track if this is a registered server
24+
};
25+
this.sockets.push(socketInfo);
26+
return socketInfo;
27+
}
28+
29+
removeSocket(socket, clientID = null) {
30+
let index = -1;
31+
if (clientID) {
32+
index = this.sockets.findIndex(s => s.clientID === clientID);
33+
} else {
34+
index = this.sockets.findIndex(s => s.socket === socket);
35+
}
36+
37+
if (index !== -1) {
38+
const socketInfo = this.sockets[index];
39+
// Clean up any pending connections for this client
40+
this.cleanupPendingConnections(socketInfo);
41+
// Remove associated UDP info
42+
this.removeUDPInfo(socketInfo.ip);
43+
this.sockets.splice(index, 1);
44+
console.log(`Removed client ${socketInfo.clientID} (${socketInfo.ip}:${socketInfo.tcpPort})`);
45+
}
46+
}
47+
48+
cleanupPendingConnections(socketInfo) {
49+
// Find any connection pairs involving this socket and notify the other party
50+
Object.entries(this.currentConnectionPairs).forEach(([id, pair]) => {
51+
if (pair.server_info === socketInfo.ip || pair.client_info === socketInfo.ip) {
52+
// Find the other party in the connection
53+
const otherSocket = this.sockets.find(s =>
54+
(pair.server_info === socketInfo.ip && s.ip === pair.client_info) ||
55+
(pair.client_info === socketInfo.ip && s.ip === pair.server_info)
56+
);
57+
58+
if (otherSocket) {
59+
// Notify the other party that the connection attempt failed
60+
otherSocket.socket.write(Buffer.from(JSON.stringify({
61+
ID: MessageTypes.ConnectionTimeout,
62+
Message: "Peer disconnected"
63+
})));
64+
}
65+
66+
// Free up the UDP connection status for both peers
67+
this.udpConnectionInfo.forEach((info) => {
68+
if (pair.server_info === info.ip || pair.client_info === info.ip) {
69+
info.status.type = StatusTypes.Free;
70+
}
71+
});
72+
73+
// Clean up the connection pair
74+
delete this.currentConnectionPairs[id];
75+
}
76+
});
77+
}
78+
79+
addUDPInfo(address, port) {
80+
const existing = this.udpConnectionInfo.find(info => info.ip === address);
81+
if (existing) {
82+
// Update the port to the most recent one observed
83+
existing.port = port;
84+
} else {
85+
this.udpConnectionInfo.push({
86+
ip: address,
87+
port,
88+
status: {
89+
id: this.connectionId,
90+
type: StatusTypes.Free
91+
}
92+
});
93+
}
94+
}
95+
96+
removeUDPInfo(ip) {
97+
const index = this.udpConnectionInfo.findIndex(info => info.ip === ip);
98+
if (index !== -1) {
99+
this.udpConnectionInfo.splice(index, 1);
100+
}
101+
}
102+
103+
updateTimeout(address) {
104+
const socket = this.sockets.find(s => s.ip === address);
105+
if (socket) {
106+
socket.timeout = Config.DEFAULT_TIMEOUT;
107+
}
108+
}
109+
110+
processTimeouts() {
111+
for (let i = this.sockets.length - 1; i >= 0; i--) {
112+
const socket = this.sockets[i];
113+
if (socket.natType !== NATTypes.Unknown) {
114+
socket.timeout--;
115+
if (socket.timeout <= 0) {
116+
console.log(`Timeout: Removing ${socket.isServer ? 'server' : 'client'} ${socket.ip}:${socket.tcpPort} (NAT type: ${socket.natType})`);
117+
this.removeSocket(socket.socket);
118+
}
119+
}
120+
}
121+
}
122+
123+
checkNATType(socket, localPort, externalPortOne, externalPortTwo) {
124+
let natType = NATTypes.Unknown;
125+
126+
if (externalPortOne !== 0 && externalPortTwo !== 0) {
127+
if (localPort === externalPortOne && localPort === externalPortTwo) {
128+
natType = NATTypes.DirectMapping;
129+
} else if (externalPortOne === externalPortTwo) {
130+
natType = NATTypes.Restricted;
131+
} else {
132+
natType = NATTypes.Symmetric;
133+
}
134+
135+
socket.write(Buffer.from(JSON.stringify({
136+
ID: MessageTypes.NATTypeResponse,
137+
NATType: natType,
138+
})));
139+
140+
// Store NAT type on socket info
141+
const socketInfo = this.sockets.find(s => s.socket === socket);
142+
if (socketInfo) {
143+
socketInfo.natType = natType;
144+
145+
// Check if this socket is for a specific connection (server-side per-client tunnel)
146+
if (socketInfo.forConnectionID) {
147+
const connectionId = socketInfo.forConnectionID;
148+
149+
// Trigger callback to send ConnectionBegin to waiting client
150+
if (this.onServerTunnelReady) {
151+
this.onServerTunnelReady(connectionId, socketInfo, natType);
152+
}
153+
}
154+
}
155+
}
156+
157+
return natType;
158+
}
159+
160+
findSocketByAddress(address) {
161+
return this.sockets.find(s => s.ip === address);
162+
}
163+
164+
updateNATTestPort(clientID, port, isFirstPort = true) {
165+
const socket = this.sockets.find(s => s.clientID === clientID);
166+
if (socket) {
167+
if (isFirstPort) {
168+
socket.externalPortOne = port;
169+
} else {
170+
socket.externalPortTwo = port;
171+
}
172+
return socket;
173+
}
174+
return null;
175+
}
176+
}
177+
178+
module.exports = ConnectionManager;

mediation_server/constants.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// NAT type enumeration
2+
const NATTypes = {
3+
DirectMapping: 0,
4+
Restricted: 1,
5+
Symmetric: 2,
6+
Unknown: -1
7+
};
8+
9+
// Message type enumeration
10+
const MessageTypes = {
11+
Connected: 0,
12+
NATTypeRequest: 1,
13+
NATTestBegin: 2,
14+
NATTest: 3,
15+
NATTypeResponse: 4,
16+
KeepAlive: 5,
17+
ConnectionRequest: 6,
18+
ConnectionBegin: 7,
19+
ServerNotAvailable: 8,
20+
HolePunchAttempt: 9,
21+
NATTunnelData: 10,
22+
SymmetricHolePunchAttempt: 11,
23+
ConnectionComplete: 12,
24+
ReceivedPeer: 13,
25+
ConnectionTimeout: 14,
26+
PublicKeyRequest: 15,
27+
PublicKeyResponse: 16,
28+
SymmetricKeyRequest: 17,
29+
SymmetricKeyResponse: 18,
30+
SymmetricKeyConfirm: 19,
31+
WireGuardPublicKeyExchange: 20,
32+
WireGuardPublicKeyHash: 21,
33+
ServerRegister: 22
34+
};
35+
36+
// Client status types
37+
const StatusTypes = {
38+
Free: 0,
39+
Busy: 1
40+
};
41+
42+
// Server configuration
43+
const Config = {
44+
TCP_PORT: 7000,
45+
UDP_PORT: 7000,
46+
NAT_TEST_PORT_ONE: 7001,
47+
NAT_TEST_PORT_TWO: 7002,
48+
DEFAULT_TIMEOUT: 10, // seconds
49+
BIND_ADDRESS: "0.0.0.0"
50+
};
51+
52+
module.exports = {
53+
NATTypes,
54+
MessageTypes,
55+
StatusTypes,
56+
Config
57+
};

0 commit comments

Comments
 (0)