@@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1313using System ;
1414using System . Collections . Concurrent ;
1515using System . Collections . Generic ;
16+ using System . Diagnostics ;
1617using System . Linq ;
1718using System . Net ;
1819using 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