/// <summary> /// Attempts to send data to the remote end point over a reliable connection. If an existing /// connection exists it will be used otherwise an attempt will be made to establish a new connection. /// </summary> /// <param name="dstEndPoint">The remote end point to send the reliable data to.</param> /// <param name="buffer">The data to send.</param> /// <param name="serverCertificateName">Optional. Only relevant for SSL streams. The common name /// that is expected for the remote SSL server.</param> /// <param name="connectionIDHint">Optional. The ID of the specific TCP connection to try and the send the message on.</param> /// <returns>If no errors SocketError.Success otherwise an error value.</returns> public override async Task <SocketError> SendSecureAsync(IPEndPoint dstEndPoint, byte[] buffer, string serverCertificateName, string connectionIDHint) { try { if (dstEndPoint == null) { throw new ArgumentException("dstEndPoint", "An empty destination was specified to Send in SIPTCPChannel."); } if (buffer == null || buffer.Length == 0) { throw new ApplicationException("An empty buffer was specified to Send in SIPTCPChannel."); } else if (DisableLocalTCPSocketsCheck == false && m_localTCPSockets.Contains(dstEndPoint.ToString())) { logger.LogWarning($"SIP {ProtDescr} Channel blocked Send to {dstEndPoint} as it was identified as a locally hosted {ProtDescr} socket.\r\n" + Encoding.UTF8.GetString(buffer)); throw new ApplicationException($"A Send call was blocked in SIP {ProtDescr} Channel due to the destination being another local TCP socket."); } else { // Lookup a client socket that is connected to the destination. If it does not exist attempt to connect a new one. SIPStreamConnection sipStreamConn = null; if (connectionIDHint != null) { m_connections.TryGetValue(connectionIDHint, out sipStreamConn); } if (sipStreamConn == null && HasConnection(dstEndPoint)) { sipStreamConn = m_connections.Where(x => x.Value.RemoteEndPoint.Equals(dstEndPoint)).First().Value; } if (sipStreamConn != null) { SendOnConnected(sipStreamConn, buffer); return(SocketError.Success); } else { return(await ConnectClientAsync(dstEndPoint, buffer, serverCertificateName)); } } } catch (SocketException sockExcp) { return(sockExcp.SocketErrorCode); } catch (ApplicationException) { throw; } catch (Exception excp) { logger.LogError("Exception (" + excp.GetType().ToString() + ") SIPTCPChannel Send (sendto=>" + dstEndPoint + "). " + excp.Message); throw; } }
/// <summary> /// Send event handler for the newer SendAsync socket call. /// </summary> /// <param name="e">The socket args for the completed send operation.</param> private void ProcessSend(SocketAsyncEventArgs e) { SIPStreamConnection streamConn = (SIPStreamConnection)e.UserToken; if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success) { // There was an error processing the last message send. Remove the disconnected socket. logger.LogWarning($"SIP {ProtDescr} Channel Socket send to {e.RemoteEndPoint} failed with socket error {e.SocketError}, removing connection."); OnSIPStreamDisconnected(streamConn, e.SocketError); } }
/// <summary> /// Sends data using the connected SSL stream. /// </summary> /// <param name="sipStreamConn">The stream connection object that holds the SSL stream.</param> /// <param name="buffer">The data to send.</param> protected override Task SendOnConnected(SIPStreamConnection sipStreamConn, byte[] buffer) { try { return(sipStreamConn.SslStream.WriteAsync(buffer, 0, buffer.Length)); } catch (SocketException sockExcp) { logger.LogWarning(sockExcp, $"SocketException SIP TLS Channel sending to {sipStreamConn.RemoteSIPEndPoint}. ErrorCode {sockExcp.SocketErrorCode}. {sockExcp}"); OnSIPStreamDisconnected(sipStreamConn, sockExcp.SocketErrorCode); throw; } }
/// <summary> /// Callback for read operations on the SSL stream. /// </summary> private void OnReadCallback(IAsyncResult ar) { SIPStreamConnection sipStreamConnection = (SIPStreamConnection)ar.AsyncState; try { int bytesRead = sipStreamConnection.SslStream.EndRead(ar); if (bytesRead == 0) { // SSL stream was disconnected by the remote end point sending a FIN or RST. logger.LogDebug($"TLS socket disconnected by {sipStreamConnection.RemoteEndPoint}."); OnSIPStreamDisconnected(sipStreamConnection, SocketError.ConnectionReset); } else { sipStreamConnection.ExtractSIPMessages(this, sipStreamConnection.SslStreamBuffer, bytesRead); sipStreamConnection.SslStream.BeginRead(sipStreamConnection.SslStreamBuffer, sipStreamConnection.RecvEndPosn, sipStreamConnection.SslStreamBuffer.Length - sipStreamConnection.RecvEndPosn, new AsyncCallback(OnReadCallback), sipStreamConnection); } } catch (SocketException sockExcp) // Occurs if the remote end gets disconnected. { OnSIPStreamDisconnected(sipStreamConnection, sockExcp.SocketErrorCode); } catch (IOException ioExcp) { if (ioExcp.InnerException is SocketException) { OnSIPStreamDisconnected(sipStreamConnection, (ioExcp.InnerException as SocketException).SocketErrorCode); } else if (ioExcp.InnerException is ObjectDisposedException) { // This exception is expected when the TLS connection is closed and this method is waiting for a receive. OnSIPStreamDisconnected(sipStreamConnection, SocketError.Disconnecting); } else { logger.LogWarning($"IOException SIPTLSChannel OnReadCallback. {ioExcp.Message}"); OnSIPStreamDisconnected(sipStreamConnection, SocketError.Fault); } } catch (Exception excp) { logger.LogWarning($"Exception SIPTLSChannel OnReadCallback. {excp.Message}"); OnSIPStreamDisconnected(sipStreamConnection, SocketError.Fault); } }
/// <summary> /// Attempts to send data to the remote end point over a reliable connection. If an existing /// connection exists it will be used otherwise an attempt will be made to establish a new connection. /// </summary> /// <param name="dstEndPoint">The remote end point to send the reliable data to.</param> /// <param name="buffer">The data to send.</param> /// <param name="serverCertificateName">Optional. Only relevant for SSL streams. The common name /// that is expected for the remote SSL server.</param> public override async Task <SocketError> SendAsync(IPEndPoint dstEndPoint, byte[] buffer, string serverCertificateName) { try { if (buffer == null || buffer.Length == 0) { throw new ApplicationException("An empty buffer was specified to Send in SIPTCPChannel."); } else if (DisableLocalTCPSocketsCheck == false && LocalTCPSockets.Contains(dstEndPoint.ToString())) { logger.LogWarning($"SIPTCPChannel blocked Send to {dstEndPoint} as it was identified as a locally hosted TCP socket.\r\n" + Encoding.UTF8.GetString(buffer)); throw new ApplicationException("A Send call was blocked in SIPTCPChannel due to the destination being another local TCP socket."); } else if (m_connectionFailures.ContainsKey(dstEndPoint.ToString())) { throw new ApplicationException($"SIP TCP channel connect attempt to {dstEndPoint} failed."); } else { SIPStreamConnection sipStreamConn = null; // Lookup a client socket that is connected to the destination. If it does not exist attempt to connect a new one. if (m_connectedSockets.ContainsKey(dstEndPoint.ToString())) { sipStreamConn = m_connectedSockets[dstEndPoint.ToString()]; SendOnConnected(sipStreamConn, buffer); return(SocketError.Success); } else { await ConnectClientAsync(dstEndPoint, buffer, serverCertificateName); return(SocketError.Success); } } } catch (SocketException sockExcp) { return(sockExcp.SocketErrorCode); } catch (ApplicationException) { throw; } catch (Exception excp) { logger.LogError("Exception (" + excp.GetType().ToString() + ") SIPTCPChannel Send (sendto=>" + dstEndPoint + "). " + excp.Message); throw; } }
/// <summary> /// Processes the socket accepts from the channel's socket listener. /// </summary> private void AcceptConnections() { logger.LogDebug( $"SIP {ProtDescr} Channel socket on {m_tcpServerListener.Server.LocalEndPoint} accept connections thread started."); while (!Closed) { try { Socket clientSocket = m_tcpServerListener.AcceptSocketAsync().Result; if (!Closed) { logger.LogDebug( $"SIP {ProtDescr} Channel connection accepted from {clientSocket.RemoteEndPoint} by {clientSocket.LocalEndPoint}."); clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); clientSocket.LingerState = new LingerOption(true, 0); SIPStreamConnection sipStmConn = new SIPStreamConnection(clientSocket, clientSocket.RemoteEndPoint as IPEndPoint, SIPProtocol); sipStmConn.SIPMessageReceived += SIPTCPMessageReceived; m_connections.TryAdd(sipStmConn.ConnectionID, sipStmConn); OnAccept(sipStmConn).Wait(); } } catch (ObjectDisposedException) { // This is a result of the transport channel being closed. Safe to ignore. //logger.LogDebug($"SIP {ProtDescr} Channel accepts for {ListeningEndPoint} cancelled."); } catch (SocketException acceptSockExcp) when(acceptSockExcp.SocketErrorCode == SocketError.Interrupted) { // This is a result of the transport channel being closed and WSACancelBlockingCall being called in WinSock2. Safe to ignore. //logger.LogDebug($"SIP {ProtDescr} Channel accepts for {ListeningEndPoint} cancelled."); } catch (AggregateException) { // This is a result of the transport channel being closed. Safe to ignore. } catch (Exception acceptExcp) { // This exception gets thrown if the remote end disconnects during the socket accept. logger.LogWarning($"Exception SIP {ProtDescr} Channel accepting socket (" + acceptExcp.GetType() + "). " + acceptExcp.Message); } } }
/// <summary> /// For TCP channel no special action is required when a new outgoing client connection is established. /// Can start receiving immeidately. /// </summary> /// <param name="streamConnection">The stream connection holding the newly connected client socket.</param> /// <param name="buffer">Optional parameter that contains the data that still needs to be sent once the connection is established.</param> protected virtual void OnClientConnect(SIPStreamConnection streamConnection, byte[] buffer, string certificateName) { SocketAsyncEventArgs recvArgs = streamConnection.RecvSocketArgs; recvArgs.AcceptSocket = streamConnection.StreamSocket; recvArgs.UserToken = streamConnection; recvArgs.Completed += IO_Completed; bool willRaise = streamConnection.StreamSocket.ReceiveAsync(recvArgs); if (!willRaise) { ProcessReceive(recvArgs); } }
/// <summary> /// For the TLS channel once the TCP client socket is connected it needs to be wrapped up in an SSL stream. /// </summary> /// <param name="streamConnection">The stream connection holding the newly connected client socket.</param> /// <param name="serverCertificateName">The expected common name on the SSL certificate supplied by the server.</param> protected override async Task OnClientConnect(SIPStreamConnection streamConnection, string serverCertificateName) { NetworkStream networkStream = new NetworkStream(streamConnection.StreamSocket, true); SslStream sslStream = new SslStream(networkStream, false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null); //DisplayCertificateInformation(sslStream); await sslStream.AuthenticateAsClientAsync(serverCertificateName); streamConnection.SslStream = sslStream; streamConnection.SslStreamBuffer = new byte[2 * SIPStreamConnection.MaxSIPTCPMessageSize]; logger.LogDebug($"SIP TLS Channel successfully upgraded client connection to SSL stream for {ListeningEndPoint}->{streamConnection.StreamSocket.RemoteEndPoint}."); sslStream.BeginRead(streamConnection.SslStreamBuffer, 0, SIPStreamConnection.MaxSIPTCPMessageSize, new AsyncCallback(OnReadCallback), streamConnection); }
/// <summary> /// For TCP channel no special action is required when accepting a new client connection. Can start receiving immediately. /// </summary> /// <param name="streamConnection">The stream connection holding the newly accepted client socket.</param> protected virtual void OnAccept(SIPStreamConnection streamConnection) { SocketAsyncEventArgs args = streamConnection.RecvSocketArgs; args.AcceptSocket = streamConnection.StreamSocket; args.UserToken = streamConnection; args.Completed += IO_Completed; bool willRaise = streamConnection.StreamSocket.ReceiveAsync(args); if (!willRaise) { ProcessReceive(args); } }
/// <summary> /// Sends data using the connected SSL stream. /// </summary> /// <param name="sipStreamConn">The stream connection object that holds the SSL stream.</param> /// <param name="buffer">The data to send.</param> protected override async void SendOnConnected(SIPStreamConnection sipStreamConn, byte[] buffer) { IPEndPoint dstEndPoint = sipStreamConn.RemoteEndPoint; try { await sipStreamConn.SslStream.WriteAsync(buffer, 0, buffer.Length); } catch (SocketException sockExcp) { logger.LogWarning($"SocketException SIP TLS Channel sending to {dstEndPoint}. ErrorCode {sockExcp.SocketErrorCode}. {sockExcp}"); OnSIPStreamDisconnected(sipStreamConn, sockExcp.SocketErrorCode); throw; } }
/// <summary> /// Event handler for a reliable SIP stream socket being disconnected. /// </summary> /// <param name="connection">The disconnected stream.</param> /// <param name="socketError">The cause of the disconnect.</param> protected void OnSIPStreamDisconnected(SIPStreamConnection connection, SocketError socketError) { try { if (connection != null) { if (socketError == SocketError.ConnectionReset) { // Connection reset seems to be the way normal closures are reported. logger.LogDebug($"SIP {ProtDescr} stream disconnected {connection.RemoteSIPEndPoint} {socketError}."); } else { logger.LogWarning($"SIP {ProtDescr} stream disconnected {connection.RemoteSIPEndPoint} {socketError}."); } if (m_connections.TryRemove(connection.ConnectionID, out _)) { var socket = connection.StreamSocket; // Important: Due to the way TCP works the end of the connection that initiates the close // is meant to go into a TIME_WAIT state. On Windows that results in the same pair of sockets // being unable to reconnect for 30s. SIP can deal with stray and duplicate messages at the // application layer so the TIME_WAIT is not that useful. In fact it TIME_WAIT is a major annoyance for SIP // as if a connection is dropped for whatever reason, such as a parser error or inactivity, it will // prevent the connection being re-established for the TIME_WAIT period. // // For this reason this implementation uses a hard RST close for client initiated socket closes. This // results in a TCP RST packet instead of the graceful FIN-ACK sequence. Two things are necessary with // WinSock2 to force the hard RST: // // - the Linger option must be set on the raw socket before binding as Linger option {1, 0}. // - the close method must be called on the socket without shutting down the stream. // Linux (WSL) note: This mechanism does not work. Calling socket close does not send the RST and instead // sends the graceful FIN-ACK. // TODO: Research if there is a way to force a socket reset with dotnet on LINUX. socket.Close(); } } } catch (Exception excp) { logger.LogError("Exception OnSIPStreamDisconnected. " + excp.Message); } }
/// <summary> /// For TCP channel no special action is required when a new outgoing client connection is established. /// Can start receiving immediately. /// </summary> /// <param name="streamConnection">The stream connection holding the newly connected client socket.</param> protected virtual Task <SocketError> OnClientConnect(SIPStreamConnection streamConnection, string certificateName) { SocketAsyncEventArgs recvArgs = streamConnection.RecvSocketArgs; recvArgs.AcceptSocket = streamConnection.StreamSocket; recvArgs.UserToken = streamConnection; recvArgs.Completed += IO_Completed; bool willRaise = streamConnection.StreamSocket.ReceiveAsync(recvArgs); if (!willRaise) { ProcessReceive(recvArgs); } return(Task.FromResult(SocketError.Success)); }
/// <summary> /// For TCP channel no special action is required when a new outgoing client connection is established. /// Can start receiving immediately. /// </summary> /// <param name="streamConnection">The stream connection holding the newly connected client socket.</param> protected virtual Task OnClientConnect(SIPStreamConnection streamConnection, string certificateName) { SocketAsyncEventArgs recvArgs = streamConnection.RecvSocketArgs; recvArgs.AcceptSocket = streamConnection.StreamSocket; recvArgs.UserToken = streamConnection; recvArgs.Completed += IO_Completed; bool willRaise = streamConnection.StreamSocket.ReceiveAsync(recvArgs); if (!willRaise) { ProcessReceive(recvArgs); } // Task.IsCompleted not available for net452. return(Task.FromResult(0)); }
/// <summary> /// Processes the socket accepts from the channel's socket listener. /// </summary> private void AcceptConnections() { Thread.CurrentThread.Name = m_acceptThreadName + m_localSIPEndPoint.Port; logger.LogDebug("SIPTCPChannel socket on " + m_localSIPEndPoint + " accept connections thread started."); while (!Closed) { try { Socket clientSocket = m_tcpServerListener.AcceptSocket(); logger.LogDebug($"SIP TCP Channel connection accepted from {clientSocket.RemoteEndPoint}."); if (!Closed) { clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); clientSocket.LingerState = new LingerOption(true, 0); SIPStreamConnection sipStmConn = new SIPStreamConnection(clientSocket, clientSocket.RemoteEndPoint as IPEndPoint, m_localSIPEndPoint.Protocol, SIPConnectionsEnum.Listener); sipStmConn.SIPMessageReceived += SIPTCPMessageReceived; lock (m_connectedSockets) { m_connectedSockets.Add(clientSocket.RemoteEndPoint.ToString(), sipStmConn); } OnAccept(sipStmConn); } } catch (SocketException acceptSockExcp) when(acceptSockExcp.SocketErrorCode == SocketError.Interrupted) { // This is a result of the transport channel being closed and WSACancelBlockingCall being called in WinSock2. Safe to ignore. logger.LogDebug($"SIPTCPChannel accepts for {m_localSIPEndPoint.ToString()} cancelled."); } catch (Exception acceptExcp) { // This exception gets thrown if the remote end disconnects during the socket accept. logger.LogWarning("Exception SIPTCPChannel accepting socket (" + acceptExcp.GetType() + "). " + acceptExcp.Message); } } }
/// <summary> /// Callback for read operations on the SSL stream. /// </summary> private void OnReadCallback(IAsyncResult ar) { SIPStreamConnection sipStreamConnection = (SIPStreamConnection)ar.AsyncState; try { int bytesRead = sipStreamConnection.SslStream.EndRead(ar); if (bytesRead == 0) { // SSL stream was disconnected by the remote end pont sending a FIN or RST. logger.LogDebug("TLS socket disconnected by {sipStreamConnection.ConnectionProps.RemoteEndPoint}."); OnSIPStreamDisconnected(sipStreamConnection.RemoteEndPoint, SocketError.ConnectionReset); } sipStreamConnection.ExtractSIPMessages(this, sipStreamConnection.SslStreamBuffer, bytesRead); sipStreamConnection.SslStream.BeginRead(sipStreamConnection.SslStreamBuffer, sipStreamConnection.RecvEndPosn, sipStreamConnection.SslStreamBuffer.Length - sipStreamConnection.RecvEndPosn, new AsyncCallback(OnReadCallback), sipStreamConnection); } catch (SocketException sockExcp) // Occurs if the remote end gets disconnected. { //logger.LogWarning($"SocketException SIPTLSChannel ReceiveCallback. Error code {sockExcp.SocketErrorCode}. {sockExcp}"); OnSIPStreamDisconnected(sipStreamConnection.RemoteEndPoint, sockExcp.SocketErrorCode); } catch (IOException ioExcp) { if (ioExcp.InnerException is SocketException) { OnSIPStreamDisconnected(sipStreamConnection.RemoteEndPoint, (ioExcp.InnerException as SocketException).SocketErrorCode); } else { logger.LogWarning($"IOException SIPTLSChannel ReceiveCallback. {ioExcp.Message}"); OnSIPStreamDisconnected(sipStreamConnection.RemoteEndPoint, SocketError.Fault); } } catch (Exception excp) { logger.LogWarning($"Exception SIPTLSChannel ReceiveCallback. {excp.Message}"); OnSIPStreamDisconnected(sipStreamConnection.RemoteEndPoint, SocketError.Fault); } }
/// <summary> /// Event handler for a reliable SIP stream socket being disconnected. /// </summary> /// <param name="connection">The disconnected stream.</param> /// <param name="socketError">The cause of the disconnect.</param> protected void OnSIPStreamDisconnected(SIPStreamConnection connection, SocketError socketError) { try { if (connection != null) { logger.LogDebug($"SIP stream disconnected {m_localSIPEndPoint.Protocol}:{connection.RemoteEndPoint} {socketError}."); lock (m_connections) { if (m_connections.TryRemove(connection.ConnectionID, out _)) { var socket = connection.StreamSocket; // Important: Due to the way TCP works the end of the connection that initiates the close // is meant to go into a TIME_WAIT state. On Windows that results in the same pair of sockets // being unable to reconnect for 30s. SIP can deal with stray and duplicate messages at the // appliction layer so the TIME_WAIT is not that useful. While not useful it is also a major annoyance // as if a connection is dropped for whatever reason, such as a parser error or inactivity, it will // prevent the connection being re-established. // // For this reason this implementation uses a hard RST close for client initiated socket closes. This // results in a TCP RST packet instead of the graceful FIN-ACK sequence. Two things are necessary with // WinSock2 to force the hard RST: // // - the Linger option must be set on the raw socket before binding as Linger option {1, 0}. // - the close method must be called on teh socket without shutting down the stream. socket.Close(); } } } } catch (Exception excp) { logger.LogError("Exception OnSIPStreamDisconnected. " + excp.Message); } }
/// <summary> /// For the TLS channel once the TCP client socket is connected it needs to be wrapped up in an SSL stream. /// </summary> /// <param name="streamConnection">The stream connection holding the newly connected client socket.</param> /// <param name="serverCertificateName">The expected common name on the SSL certificate supplied by the server.</param> protected override async Task <SocketError> OnClientConnect(SIPStreamConnection streamConnection, string serverCertificateName) { NetworkStream networkStream = new NetworkStream(streamConnection.StreamSocket, true); SslStream sslStream = new SslStream(networkStream, false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null); //DisplayCertificateInformation(sslStream); var timeoutTask = Task.Delay(TLS_ATTEMPT_CONNECT_TIMEOUT); var sslStreamTask = m_clientCertificates != null?sslStream.AuthenticateAsClientAsync(serverCertificateName, m_clientCertificates, SslProtocols.None, false) : sslStream.AuthenticateAsClientAsync(serverCertificateName); await Task.WhenAny(sslStreamTask, timeoutTask).ConfigureAwait(false); if (sslStreamTask.IsCompleted) { if (!sslStream.IsAuthenticated) { logger.LogWarning($"SIP TLS channel failed to establish SSL stream with {streamConnection.RemoteSIPEndPoint}."); networkStream.Close(CLOSE_CONNECTION_TIMEOUT); return(SocketError.ProtocolNotSupported); } else { streamConnection.SslStream = sslStream; streamConnection.SslStreamBuffer = new byte[2 * SIPStreamConnection.MaxSIPTCPMessageSize]; logger.LogDebug($"SIP TLS Channel successfully upgraded client connection to SSL stream for {ListeningSIPEndPoint}->{streamConnection.RemoteSIPEndPoint}."); sslStream.BeginRead(streamConnection.SslStreamBuffer, 0, SIPStreamConnection.MaxSIPTCPMessageSize, new AsyncCallback(OnReadCallback), streamConnection); return(SocketError.Success); } } else { logger.LogWarning($"SIP TLS channel timed out attempting to establish SSL stream with {streamConnection.RemoteSIPEndPoint}."); networkStream.Close(CLOSE_CONNECTION_TIMEOUT); return(SocketError.TimedOut); } }
/// <summary> /// Receive event handler for the newer ReceiveAsync socket call. /// </summary> /// <param name="e">The socket args for the completed receive operation.</param> private void ProcessReceive(SocketAsyncEventArgs e) { SIPStreamConnection streamConn = (SIPStreamConnection)e.UserToken; try { if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success) { byte[] buffer = streamConn.RecvSocketArgs.Buffer; streamConn.ExtractSIPMessages(this, buffer, e.BytesTransferred); streamConn.RecvSocketArgs.SetBuffer(buffer, streamConn.RecvEndPosn, buffer.Length - streamConn.RecvEndPosn); bool willRaiseEvent = streamConn.StreamSocket.ReceiveAsync(e); if (!willRaiseEvent) { ProcessReceive(e); } } else { OnSIPStreamDisconnected(streamConn, e.SocketError); } } catch (SocketException sockExcp) { OnSIPStreamDisconnected(streamConn, sockExcp.SocketErrorCode); } catch (Exception excp) { // There was an error processing the last message received. Remove the disconnected socket. logger.LogError( $"Exception processing SIP {ProtDescr} stream receive on read from {e.RemoteEndPoint} closing connection. {excp.Message}"); OnSIPStreamDisconnected(streamConn, SocketError.Fault); } }
/// <summary> /// For the TLS channel the SSL stream must be created and any authentication actions undertaken. /// </summary> /// <param name="streamConnection">The stream connection holding the newly accepted client socket.</param> protected override async Task OnAccept(SIPStreamConnection streamConnection) { NetworkStream networkStream = new NetworkStream(streamConnection.StreamSocket, true); SslStream sslStream = new SslStream(networkStream, false); await sslStream.AuthenticateAsServerAsync(m_serverCertificate); logger.LogDebug($"SIP TLS Channel successfully upgraded accepted client to SSL stream for {ListeningEndPoint}->{streamConnection.StreamSocket.RemoteEndPoint}."); //// Display the properties and settings for the authenticated stream. ////DisplaySecurityLevel(sslStream); ////DisplaySecurityServices(sslStream); ////DisplayCertificateInformation(sslStream); ////DisplayStreamProperties(sslStream); //// Set timeouts for the read and write to 5 seconds. //sslStream.ReadTimeout = 5000; //sslStream.WriteTimeout = 5000; streamConnection.SslStream = sslStream; streamConnection.SslStreamBuffer = new byte[2 * SIPStreamConnection.MaxSIPTCPMessageSize]; sslStream.BeginRead(streamConnection.SslStreamBuffer, 0, SIPStreamConnection.MaxSIPTCPMessageSize, new AsyncCallback(OnReadCallback), streamConnection); }
/// <summary> /// Periodically checks the established connections and closes any that have not had a transmission for a specified /// period or where the number of connections allowed per IP address has been exceeded. /// </summary> private void PruneConnections() { try { Task.Delay(PRUNE_CONNECTIONS_INTERVAL, m_cts.Token).Wait(); while (!Closed) { bool checkComplete = false; while (!checkComplete) { try { SIPStreamConnection inactiveConnection = null; lock (m_connections) { var inactiveConnectionKey = (from connection in m_connections where connection.Value.LastTransmission < DateTime.Now.AddMinutes(PRUNE_NOTRANSMISSION_MINUTES * -1) select connection.Key).FirstOrDefault(); if (inactiveConnectionKey != null) { inactiveConnection = m_connections[inactiveConnectionKey]; m_connections.TryRemove(inactiveConnectionKey, out _); } } if (inactiveConnection != null) { logger.LogDebug( $"Pruning inactive connection on {ProtDescr} {ListeningEndPoint} to remote end point {inactiveConnection.RemoteEndPoint}."); inactiveConnection.StreamSocket.Close(); } else { checkComplete = true; } } catch (SocketException sockExcp) { // Will be thrown if the socket is already closed. logger.LogWarning( $"Socket error in PruneConnections. {sockExcp.Message} ({sockExcp.ErrorCode})."); } catch (Exception pruneExcp) { logger.LogError("Exception PruneConnections (pruning). " + pruneExcp.Message); checkComplete = true; } } Task.Delay(PRUNE_CONNECTIONS_INTERVAL, m_cts.Token).Wait(); checkComplete = false; } logger.LogDebug($"SIP {ProtDescr} Channel socket on {ListeningEndPoint} pruning connections halted."); } catch (OperationCanceledException) { } catch (AggregateException) { } // This gets thrown if task is cancelled. catch (Exception excp) { logger.LogError($"Exception SIP {ProtDescr} Channel PruneConnections. " + excp.Message); } }
/// <summary> /// Attempts to create a client TCP socket connection to a remote end point. /// </summary> /// <param name="dstEndPoint">The remote TCP end point to attempt to connect to.</param> /// <param name="buffer">An optional buffer that if set can contain data to transmit immediately after connecting.</param> /// <returns>If successful a connected client socket or null if not.</returns> internal async Task <SocketError> ConnectClientAsync(IPEndPoint dstEndPoint, byte[] buffer, string serverCertificateName) { try { // No existing TCP connection to the destination. Attempt a new socket connection. IPAddress localAddress = ListeningIPAddress; if (ListeningIPAddress == IPAddress.Any) { localAddress = NetServices.GetLocalAddressForRemote(dstEndPoint.Address); } IPEndPoint localEndPoint = new IPEndPoint(localAddress, Port); logger.LogDebug( $"ConnectAsync SIP {ProtDescr} Channel local end point of {localEndPoint} selected for connection to {dstEndPoint}."); Socket clientSocket = new Socket(dstEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); clientSocket.LingerState = new LingerOption(true, 0); clientSocket.Bind(localEndPoint); SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs { RemoteEndPoint = dstEndPoint }; // NOTE: The approach of setting the buffer on the connect args worked properly on Windows BUT // not so on Linux. Since it's such a tiny saving skip setting the buffer on the connect and // do the send once the sockets are connected (use SendOnConnected). // If this is a TCP channel can take a shortcut and set the first send payload on the connect args. //if (buffer != null && buffer.Length > 0 && serverCertificateName == null) //{ // connectArgs.SetBuffer(buffer, 0, buffer.Length); //} logger.LogDebug($"Attempting TCP connection from {localEndPoint} to {dstEndPoint}."); // Attempt to connect. TaskCompletionSource <SocketError> connectTcs = new TaskCompletionSource <SocketError>(TaskCreationOptions.RunContinuationsAsynchronously); connectArgs.Completed += (sender, sockArgs) => { if (sockArgs.LastOperation == SocketAsyncOperation.Connect) { connectTcs.SetResult(sockArgs.SocketError); } }; bool willRaiseEvent = clientSocket.ConnectAsync(connectArgs); if (!willRaiseEvent) { if (connectArgs.LastOperation == SocketAsyncOperation.Connect) { connectTcs.SetResult(connectArgs.SocketError); } } var connectResult = await connectTcs.Task.ConfigureAwait(false); logger.LogDebug( $"ConnectAsync SIP {ProtDescr} Channel connect completed result for {localEndPoint}->{dstEndPoint} {connectResult}."); if (connectResult != SocketError.Success) { logger.LogWarning( $"SIP {ProtDescr} Channel send to {dstEndPoint} failed. Attempt to create a client socket failed."); } else { SIPStreamConnection sipStmConn = new SIPStreamConnection(clientSocket, clientSocket.RemoteEndPoint as IPEndPoint, SIPProtocol); sipStmConn.SIPMessageReceived += SIPTCPMessageReceived; m_connections.TryAdd(sipStmConn.ConnectionID, sipStmConn); await OnClientConnect(sipStmConn, serverCertificateName).ConfigureAwait(false); await SendOnConnected(sipStmConn, buffer).ConfigureAwait(false); } return(connectResult); } catch (Exception excp) { logger.LogError($"Exception ConnectClientAsync. {excp.Message}"); return(SocketError.Fault); } }
/// <summary> /// Attempts to send data to the remote end point over a reliable connection. If an existing /// connection exists it will be used otherwise an attempt will be made to establish a new connection. /// </summary> /// <param name="dstSIPEndPoint">The remote SIP end point to send the reliable data to.</param> /// <param name="buffer">The data to send.</param> /// <param name="serverCertificateName">Optional. Only relevant for SSL streams. The common name /// that is expected for the remote SSL server.</param> /// <param name="canInitiateConnection">Indicates whether this send should initiate a connection if needed. /// The typical case is SIP requests can initiate new connections but responses should not. Responses should /// only be sent on the same TCP or TLS connection that the original request was received on.</param> /// <param name="connectionIDHint">Optional. The ID of the specific TCP connection to try and the send the message on.</param> /// <returns>If no errors SocketError.Success otherwise an error value.</returns> public override Task <SocketError> SendSecureAsync( SIPEndPoint dstSIPEndPoint, byte[] buffer, string serverCertificateName, bool canInitiateConnection, string connectionIDHint) { try { if (dstSIPEndPoint == null) { throw new ArgumentException(nameof(dstSIPEndPoint), "An empty destination was specified to Send in SIPTCPChannel."); } if (buffer == null || buffer.Length == 0) { throw new ApplicationException("An empty buffer was specified to Send in SIPTCPChannel."); } else if (!DisableLocalTCPSocketsCheck && NetServices.LocalIPAddresses.Contains(dstSIPEndPoint.Address) && Port == dstSIPEndPoint.Port) { logger.LogWarning($"SIP {ProtDescr} Channel blocked Send to {dstSIPEndPoint} as it was identified as a locally hosted {ProtDescr} socket.\r\n" + SIPConstants.DEFAULT_ENCODING.GetString(buffer)); throw new ApplicationException($"A Send call was blocked in SIP {ProtDescr} Channel due to the destination being another local TCP socket."); } else { IPEndPoint dstEndPoint = dstSIPEndPoint.GetIPEndPoint(m_isDualMode); // Lookup a client socket that is connected to the destination. If it does not exist attempt to connect a new one. SIPStreamConnection sipStreamConn = null; if (connectionIDHint != null) { m_connections.TryGetValue(connectionIDHint, out sipStreamConn); } if (sipStreamConn == null && HasConnection(dstSIPEndPoint)) { sipStreamConn = m_connections.Where(x => x.Value.RemoteSIPEndPoint.IsSocketEqual(dstSIPEndPoint)).First().Value; } if (sipStreamConn != null) { SendOnConnected(sipStreamConn, buffer); return(Task.FromResult(SocketError.Success)); } else if (canInitiateConnection) { return(ConnectClientAsync(dstEndPoint, buffer, serverCertificateName)); } else { logger.LogWarning($"SIP {ProtDescr} Channel did not have an existing connection for send to {dstSIPEndPoint} and requested not to initiate a connection."); return(Task.FromResult(SocketError.NotConnected)); } } } catch (SocketException sockExcp) { return(Task.FromResult(sockExcp.SocketErrorCode)); } catch (ApplicationException) { throw; } catch (Exception excp) { logger.LogError($"Exception SIPTCPChannel Send (sendto=>{dstSIPEndPoint}). {excp}"); throw; } }
/// <summary> /// Attempts to create a client TCP socket connection to a remote end point. /// </summary> /// <param name="dstEndPoint">The remote TCP end point to attempt to connect to.</param> /// <param name="buffer">An optional buffer that if set can contain data to transmit immediately after connecting.</param> /// <returns>If successful a connected client socket or null if not.</returns> public async Task ConnectClientAsync(IPEndPoint dstEndPoint, byte[] buffer, string serverCertificateName) { // No existing TCP connection to the destination. Attempt a new socket connection. IPEndPoint localEndPoint = m_localSIPEndPoint.GetIPEndPoint(); Socket clientSocket = new Socket(dstEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); clientSocket.LingerState = new LingerOption(true, 0); clientSocket.Bind(localEndPoint); SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs { AcceptSocket = clientSocket, RemoteEndPoint = dstEndPoint }; // If this is a TCP channel can take a shortcut and set the first send payload on the connect args. if (buffer != null && buffer.Length > 0 && serverCertificateName == null) { connectArgs.SetBuffer(buffer, 0, buffer.Length); } // Attempt to connect. TaskCompletionSource <SocketError> connectTcs = new TaskCompletionSource <SocketError>(); connectArgs.Completed += (sender, sockArgs) => { if (sockArgs.LastOperation == SocketAsyncOperation.Connect) { connectTcs.SetResult(sockArgs.SocketError); } }; bool willRaiseEvent = clientSocket.ConnectAsync(connectArgs); if (!willRaiseEvent) { if (connectArgs.LastOperation == SocketAsyncOperation.Connect) { connectTcs.SetResult(connectArgs.SocketError); } } var connectResult = await connectTcs.Task; logger.LogDebug($"ConnectAsync SIP TCP Channel connect completed result for {localEndPoint}->{dstEndPoint} {connectResult}."); if (connectResult != SocketError.Success) { logger.LogWarning($"SIP TCP Channel sent to {dstEndPoint} failed. Attempt to create a client socket failed."); lock (m_connectionFailures) { m_connectionFailures.Add(dstEndPoint.ToString(), DateTime.Now); } throw new ApplicationException($"Failed to establish TCP connection to {dstEndPoint}."); } else { SIPStreamConnection sipStmConn = new SIPStreamConnection(clientSocket, clientSocket.RemoteEndPoint as IPEndPoint, m_localSIPEndPoint.Protocol, SIPConnectionsEnum.Caller); sipStmConn.SIPMessageReceived += SIPTCPMessageReceived; lock (m_connectedSockets) { m_connectedSockets.Add(clientSocket.RemoteEndPoint.ToString(), sipStmConn); } OnClientConnect(sipStmConn, buffer, serverCertificateName); } }
/// <summary> /// Periodically checks the established connections and closes any that have not had a transmission for a specified /// period or where the number of connections allowed per IP address has been exceeded. Only relevant for connection /// oriented channels such as TCP and TLS. /// </summary> protected void PruneConnections(string threadName) { try { Thread.CurrentThread.Name = threadName; Thread.Sleep(INITIALPRUNE_CONNECTIONS_DELAY); while (!Closed) { bool checkComplete = false; while (!checkComplete) { try { SIPStreamConnection inactiveConnection = null; Dictionary <string, SIPStreamConnection> connections = GetConnectionsList(); lock (connections) { var inactiveConnectionKey = (from connection in connections where connection.Value.LastTransmission < DateTime.Now.AddMinutes(PRUNE_NOTRANSMISSION_MINUTES * -1) select connection.Key).FirstOrDefault(); if (inactiveConnectionKey != null) { inactiveConnection = connections[inactiveConnectionKey]; connections.Remove(inactiveConnectionKey); } } if (inactiveConnection != null) { logger.LogDebug($"Pruning inactive connection on {SIPChannelContactURI}to remote end point {inactiveConnection.RemoteEndPoint}."); inactiveConnection.StreamSocket.Close(); } else { checkComplete = true; } } catch (SocketException) { // Will be thrown if the socket is already closed. } catch (Exception pruneExcp) { logger.LogError("Exception PruneConnections (pruning). " + pruneExcp.Message); checkComplete = true; } } Thread.Sleep(PRUNE_CONNECTIONS_INTERVAL); checkComplete = false; } logger.LogDebug("SIPChannel socket on " + m_localSIPEndPoint.ToString() + " pruning connections halted."); } catch (Exception excp) { logger.LogError("Exception SIPChannel PruneConnections. " + excp.Message); } }
/// <summary> /// Attempts to create a client TCP socket connection to a remote end point. /// </summary> /// <param name="dstEndPoint">The remote TCP end point to attempt to connect to.</param> /// <param name="buffer">An optional buffer that if set can contain data to transmit immediately after connecting.</param> /// <returns>If successful a connected client socket or null if not.</returns> internal async Task <SocketError> ConnectClientAsync(IPEndPoint dstEndPoint, byte[] buffer, string serverCertificateName) { try { // Map IPv4 to IPv6 for dual mode socket sends. if (dstEndPoint.AddressFamily == AddressFamily.InterNetwork && m_isDualMode) { dstEndPoint = new IPEndPoint(dstEndPoint.Address.MapToIPv6(), dstEndPoint.Port); } logger.LogDebug($"ConnectClientAsync SIP {ProtDescr} Channel local end point of {ListeningSIPEndPoint} selected for connection to {dstEndPoint}."); Socket clientSocket = new Socket(ListeningIPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); clientSocket.LingerState = new LingerOption(true, 0); if (m_isDualMode && ListeningIPAddress.AddressFamily == AddressFamily.InterNetworkV6) { clientSocket.DualMode = true; } clientSocket.Bind(ListeningEndPoint); SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs { RemoteEndPoint = dstEndPoint }; // NOTE: The approach of setting the buffer on the connect args worked properly on Windows BUT // not so on Linux. Since it's such a tiny saving skip setting the buffer on the connect and // do the send once the sockets are connected (use SendOnConnected). // If this is a TCP channel can take a shortcut and set the first send payload on the connect args. //if (buffer != null && buffer.Length > 0 && serverCertificateName == null) //{ // connectArgs.SetBuffer(buffer, 0, buffer.Length); //} logger.LogDebug($"Attempting TCP connection from {ListeningSIPEndPoint} to {dstEndPoint}."); // Attempt to connect. TaskCompletionSource <SocketError> connectTcs = new TaskCompletionSource <SocketError>(TaskCreationOptions.RunContinuationsAsynchronously); connectArgs.Completed += (sender, sockArgs) => { if (sockArgs.LastOperation == SocketAsyncOperation.Connect) { connectTcs.SetResult(sockArgs.SocketError); } }; bool willRaiseEvent = clientSocket.ConnectAsync(connectArgs); if (!willRaiseEvent && connectArgs.LastOperation == SocketAsyncOperation.Connect) { connectTcs.SetResult(connectArgs.SocketError); } var timeoutTask = Task.Delay(TCP_ATTEMPT_CONNECT_TIMEOUT); var connectResult = await Task.WhenAny(connectTcs.Task, timeoutTask).ConfigureAwait(false); if (timeoutTask.IsCompleted) { logger.LogWarning($"SIP {ProtDescr} channel timed out attempting to establish a connection to {dstEndPoint}."); return(SocketError.TimedOut); } else if (connectTcs.Task.Result != SocketError.Success) { logger.LogWarning($"SIP {ProtDescr} Channel send to {dstEndPoint} failed. Attempt to create a client socket failed with {connectTcs.Task.Result}."); return(connectTcs.Task.Result); } else { logger.LogDebug($"ConnectAsync SIP {ProtDescr} Channel connect completed result for {ListeningSIPEndPoint}->{dstEndPoint} {connectTcs.Task.Result}."); var remoteSIPEndPoint = new SIPEndPoint(SIPProtocol, clientSocket.RemoteEndPoint as IPEndPoint); SIPStreamConnection sipStmConn = new SIPStreamConnection(clientSocket, SIPEncoding, SIPBodyEncoding, remoteSIPEndPoint, SIPProtocol); sipStmConn.SIPMessageReceived += SIPTCPMessageReceived; var postConnectResult = await OnClientConnect(sipStmConn, serverCertificateName).ConfigureAwait(false); if (postConnectResult != SocketError.Success) { logger.LogWarning($"SIP {ProtDescr} Channel send to {dstEndPoint} failed. Attempt to connect to server at {dstEndPoint} failed with {postConnectResult}."); } else { m_connections.TryAdd(sipStmConn.ConnectionID, sipStmConn); await SendOnConnected(sipStmConn, buffer).ConfigureAwait(false); } return(postConnectResult); } } catch (Exception excp) { logger.LogError($"Exception ConnectClientAsync. {excp}"); return(SocketError.Fault); } }