Skip to content

Commit 3543d02

Browse files
authored
Added a minimal rogue client detection mechanism at the transport level (#2850)
Clients that are failing too often to pass the security validationin a certain interval of time with the Basic128 security profile are now tracked and blocked.
1 parent 76b5318 commit 3543d02

File tree

2 files changed

+278
-1
lines changed

2 files changed

+278
-1
lines changed

Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1111
*/
1212

1313
using System;
14+
using System.Net;
1415
using System.Net.Sockets;
1516
using System.Security.Cryptography.X509Certificates;
1617
using Microsoft.Extensions.Logging;
@@ -267,6 +268,18 @@ protected void ForceChannelFault(ServiceResult reason)
267268

268269
if (close)
269270
{
271+
// mark the RemoteAddress as potential problematic if Basic128Rsa15
272+
if ((SecurityPolicyUri == SecurityPolicies.Basic128Rsa15) &&
273+
(reason.StatusCode == StatusCodes.BadSecurityChecksFailed || reason.StatusCode == StatusCodes.BadTcpMessageTypeInvalid))
274+
{
275+
var tcpTransportListener = m_listener as TcpTransportListener;
276+
if (tcpTransportListener != null)
277+
{
278+
tcpTransportListener.MarkAsPotentialProblematic
279+
(((IPEndPoint)Socket.RemoteEndpoint).Address);
280+
}
281+
}
282+
270283
// close channel immediately.
271284
ChannelFaulted();
272285
}

Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs

Lines changed: 265 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1313
using System;
1414
using System.Collections.Concurrent;
1515
using System.Collections.Generic;
16+
using System.Diagnostics;
1617
using System.Linq;
1718
using System.Net;
1819
using System.Net.Sockets;
@@ -37,6 +38,233 @@ public override ITransportListener Create()
3738
}
3839
}
3940

41+
/// <summary>
42+
/// Represents a potential problematic ActiveClient
43+
/// </summary>
44+
public class ActiveClient
45+
{
46+
#region Properties
47+
/// <summary>
48+
/// Time of the last recorded problematic action
49+
/// </summary>
50+
public int LastActionTicks
51+
{
52+
get
53+
{
54+
return m_lastActionTicks;
55+
}
56+
set
57+
{
58+
m_lastActionTicks = value;
59+
}
60+
}
61+
62+
/// <summary>
63+
/// Counter for number of recorded potential problematic actions
64+
/// </summary>
65+
public int ActiveActionCount
66+
{
67+
get
68+
{
69+
return m_actionCount;
70+
}
71+
set
72+
{
73+
m_actionCount = value;
74+
}
75+
}
76+
77+
/// <summary>
78+
/// Ticks until the client is Blocked
79+
/// </summary>
80+
public int BlockedUntilTicks
81+
{
82+
get
83+
{
84+
return m_blockedUntilTicks;
85+
}
86+
set
87+
{
88+
m_blockedUntilTicks = value;
89+
}
90+
}
91+
#endregion
92+
93+
#region Private members
94+
int m_lastActionTicks;
95+
int m_actionCount;
96+
int m_blockedUntilTicks;
97+
#endregion
98+
}
99+
100+
/// <summary>
101+
/// Manages clients with potential problematic activities
102+
/// </summary>
103+
public class ActiveClientTracker : IDisposable
104+
{
105+
#region Public
106+
/// <summary>
107+
/// Constructor
108+
/// </summary>
109+
public ActiveClientTracker()
110+
{
111+
m_cleanupTimer = new Timer(CleanupExpiredEntries, null, m_kCleanupIntervalMs, m_kCleanupIntervalMs);
112+
}
113+
114+
/// <summary>
115+
/// Checks if an IP address is currently blocked
116+
/// </summary>
117+
/// <param name="ipAddress"></param>
118+
/// <returns></returns>
119+
public bool IsBlocked(IPAddress ipAddress)
120+
{
121+
if (m_activeClients.TryGetValue(ipAddress, out ActiveClient client))
122+
{
123+
int currentTicks = HiResClock.TickCount;
124+
return IsBlockedTicks(client.BlockedUntilTicks, currentTicks);
125+
}
126+
return false;
127+
}
128+
129+
/// <summary>
130+
/// Adds a potential problematic action entry for a client
131+
/// </summary>
132+
/// <param name="ipAddress"></param>
133+
public void AddClientAction(IPAddress ipAddress)
134+
{
135+
int currentTicks = HiResClock.TickCount;
136+
137+
m_activeClients.AddOrUpdate(ipAddress,
138+
// If client is new , create a new entry
139+
key => new ActiveClient {
140+
LastActionTicks = currentTicks,
141+
ActiveActionCount = 1,
142+
BlockedUntilTicks = 0
143+
},
144+
// If the client exists, update its entry
145+
(key, existingEntry) => {
146+
// If IP currently blocked simply do nothing
147+
if (IsBlockedTicks(existingEntry.BlockedUntilTicks, currentTicks))
148+
{
149+
return existingEntry;
150+
}
151+
152+
// Elapsed time since last recorded action
153+
int elapsedSinceLastRecAction = currentTicks - existingEntry.LastActionTicks;
154+
155+
if (elapsedSinceLastRecAction <= m_kActionsIntervalMs)
156+
{
157+
existingEntry.ActiveActionCount++;
158+
159+
if (existingEntry.ActiveActionCount > m_kNrActionsTillBlock)
160+
{
161+
// Block the IP
162+
existingEntry.BlockedUntilTicks = currentTicks + m_kBlockDurationMs;
163+
Utils.LogError("RemoteClient IPAddress: {0} blocked for {1} ms due to exceeding {2} actions under {3} ms ",
164+
ipAddress.ToString(),
165+
m_kBlockDurationMs,
166+
m_kNrActionsTillBlock,
167+
m_kActionsIntervalMs);
168+
169+
}
170+
}
171+
else
172+
{
173+
// Reset the count as the last action was outside the interval
174+
existingEntry.ActiveActionCount = 1;
175+
}
176+
177+
existingEntry.LastActionTicks = currentTicks;
178+
179+
return existingEntry;
180+
}
181+
);
182+
}
183+
184+
/// <summary>
185+
/// Dispose the cleanup timer
186+
/// </summary>
187+
public void Dispose()
188+
{
189+
m_cleanupTimer?.Dispose();
190+
}
191+
192+
#endregion
193+
#region Private methods
194+
195+
/// <summary>
196+
/// Periodically cleans up expired active client entries to avoid memory leak and unblock clients whose duration has expired.
197+
/// </summary>
198+
/// <param name="state"></param>
199+
private void CleanupExpiredEntries(object state)
200+
{
201+
int currentTicks = HiResClock.TickCount;
202+
203+
foreach (var entry in m_activeClients)
204+
{
205+
IPAddress clientIp = entry.Key;
206+
ActiveClient rClient = entry.Value;
207+
208+
// Unblock client if blocking duration has been exceeded
209+
if (rClient.BlockedUntilTicks != 0 && !IsBlockedTicks(rClient.BlockedUntilTicks, currentTicks))
210+
{
211+
rClient.BlockedUntilTicks = 0;
212+
rClient.ActiveActionCount = 0;
213+
Utils.LogDebug("Active Client with IP {0} is now unblocked, blocking duration of {1} ms has been exceeded",
214+
clientIp.ToString(),
215+
m_kBlockDurationMs);
216+
}
217+
218+
// Remove clients that haven't had any potential problematic actions in the last m_kEntryExpirationMs interval
219+
int elapsedSinceBadActionTicks = currentTicks - rClient.LastActionTicks;
220+
if (elapsedSinceBadActionTicks > m_kEntryExpirationMs)
221+
{
222+
// Even if TryRemove fails it will most probably succeed at the next execution
223+
if (m_activeClients.TryRemove(clientIp, out _))
224+
{
225+
Utils.LogDebug("Active Client with IP {0} is not tracked any longer, hasn't had actions for more than {1} ms",
226+
clientIp.ToString(),
227+
m_kEntryExpirationMs);
228+
}
229+
}
230+
}
231+
}
232+
233+
/// <summary>
234+
/// Determines if the IP is currently blocked based on the block expiration ticks and current ticks
235+
/// </summary>
236+
/// <param name="blockedUntilTicks"></param>
237+
/// <param name="currentTicks"></param>
238+
/// <returns></returns>
239+
private bool IsBlockedTicks(int blockedUntilTicks, int currentTicks)
240+
{
241+
if (blockedUntilTicks == 0)
242+
{
243+
return false;
244+
}
245+
// C# signed arithmetic
246+
int diff = blockedUntilTicks - currentTicks;
247+
// If currentTicks < blockedUntilTicks then it is still blocked
248+
// Works even if TickCount has wrapped around due to C# signed integer arithmetic
249+
return diff > 0;
250+
}
251+
252+
253+
#endregion
254+
#region Private members
255+
private ConcurrentDictionary<IPAddress, ActiveClient> m_activeClients = new ConcurrentDictionary<IPAddress, ActiveClient>();
256+
257+
private const int m_kActionsIntervalMs = 10_000;
258+
private const int m_kNrActionsTillBlock = 3;
259+
260+
private const int m_kBlockDurationMs = 30_000; // 30 seconds
261+
private const int m_kCleanupIntervalMs = 15_000;
262+
private const int m_kEntryExpirationMs = 600_000; // 10 minutes
263+
264+
private Timer m_cleanupTimer;
265+
#endregion
266+
}
267+
40268
/// <summary>
41269
/// Manages the transport for a UA TCP server.
42270
/// </summary>
@@ -331,6 +559,12 @@ public void Start()
331559
{
332560
lock (m_lock)
333561
{
562+
// Track potential problematic client behavior only if Basic128Rsa15 security policy is offered
563+
if (m_descriptions != null && m_descriptions.Any(d => d.SecurityPolicyUri == SecurityPolicies.Basic128Rsa15))
564+
{
565+
m_activeClientTracker = new ActiveClientTracker();
566+
}
567+
334568
// ensure a valid port.
335569
int port = m_uri.Port;
336570

@@ -505,16 +739,44 @@ public void CertificateUpdate(
505739
}
506740
#endregion
507741

742+
#region Internal
743+
/// <summary>
744+
/// Mark a remote endpoint as potential problematic
745+
/// </summary>
746+
/// <param name="remoteEndpoint"></param>
747+
internal void MarkAsPotentialProblematic(IPAddress remoteEndpoint)
748+
{
749+
Utils.LogDebug("MarkClientAsPotentialProblematic address: {0} ", remoteEndpoint.ToString());
750+
m_activeClientTracker?.AddClientAction(remoteEndpoint);
751+
}
752+
#endregion
753+
508754
#region Socket Event Handler
509755
/// <summary>
510756
/// Handles a new connection.
511757
/// </summary>
512758
private void OnAccept(object sender, SocketAsyncEventArgs e)
513759
{
760+
514761
TcpListenerChannel channel = null;
515762
bool repeatAccept = false;
516763
do
517764
{
765+
bool isBlocked = false;
766+
767+
// Track potential problematic client behavior only if Basic128Rsa15 security policy is offered
768+
if (m_activeClientTracker != null)
769+
{
770+
// Filter out the Remote IP addresses which are detected with potential problematic behavior
771+
IPAddress ipAddress = ((IPEndPoint)e?.AcceptSocket?.RemoteEndPoint)?.Address;
772+
if (ipAddress != null && m_activeClientTracker.IsBlocked(ipAddress))
773+
{
774+
Utils.LogDebug("OnAccept: RemoteEndpoint address: {0} refused access for behaving as potential problematic ",
775+
((IPEndPoint)e.AcceptSocket.RemoteEndPoint).Address.ToString());
776+
isBlocked = true;
777+
}
778+
}
779+
518780
repeatAccept = false;
519781
lock (m_lock)
520782
{
@@ -526,7 +788,7 @@ private void OnAccept(object sender, SocketAsyncEventArgs e)
526788
}
527789

528790
var channels = m_channels;
529-
if (channels != null)
791+
if (channels != null && !isBlocked)
530792
{
531793
// TODO: .Count is flagged as hotpath, implement separate counter
532794
int channelCount = channels.Count;
@@ -821,6 +1083,8 @@ private void SetUri(Uri baseAddress, string relativeAddress)
8211083
private int m_inactivityDetectPeriod;
8221084
private Timer m_inactivityDetectionTimer;
8231085
private int m_maxChannelCount;
1086+
1087+
private ActiveClientTracker m_activeClientTracker;
8241088
#endregion
8251089
}
8261090

0 commit comments

Comments
 (0)