// NOTE: It's not always strictly a data error. So this method could use a better name. /// <summary> /// Call this when a RemotePeer provides invalid network data or otherwise needs to be disconnected. /// If the remote peer is a client, they will stop providing network messages and will be /// disconnected/kicked (depending on our authority) at the next call to Update. /// If the remote peer is the server, we will disconnect from the network and a /// <see cref="NetworkDisconnectionException" /> will be thrown. /// </summary> /// <remarks> /// Application layer should call this when it cannot throw back into the network system /// (from its event handlers) to get an orderly disconnection (ie: outside of Update). /// </remarks> public void NetworkDataError(RemotePeer remotePeer, Exception exception) { if (remotePeer == null) { throw new ArgumentNullException("remotePeer"); } if (peerManager == null) { throw new InvalidOperationException("Network not active"); } // (really we should be checking whether the remote peer is our's) Log("Network data error from " + remotePeer.PeerInfo); LogException(exception); // Stop any more messages from being read, until we can disconnect them properly! remotePeer.ClearMessageQueue(); if (remotePeer.PeerInfo.IsServer) { DisconnectAndThrow(UserVisibleStrings.ErrorInDataFromServer); } else { networkDataErrorKickList.Add(remotePeer); } }
public PendingConnection(double timeOut, RemotePeer remotePeer) { this.timeOut = timeOut; this.remotePeer = remotePeer; this.incommingConnections = new SortedSet <int>(); this.outgoingConnections = new SortedSet <int>(); }
private void HandleReportDisputeDisconnection(NetIncomingMessage message) { RemotePeer disputer = (message.SenderConnection.Tag as RemotePeer); int reporterId; try { reporterId = message.ReadInt32(); } catch (Exception e) { throw new ProtocolException("Bad disconnection dispute", e); } // Try and find the matching pending disconnect: for (int i = pendingDisconnects.Count - 1; i >= 0; i--) { if (pendingDisconnects[i].failedConnectFrom == reporterId && pendingDisconnects[i].failedConnectTo == disputer.PeerInfo.ConnectionId) { RemotePeer reporter = FindRemotePeerById(reporterId); owner.Log(disputer.PeerInfo.ToString() + " successfully disputed disconnection by " + reporter.PeerInfo.ToString()); Kick(reporter); } } // If we get to here, there was no matching pending disconnect. But this could be because they were already disconnected. }
void BecomeApplicationConnected(RemotePeer remotePeer) { Debug.Assert(!remotePeer.PeerInfo.IsApplicationConnected); remotePeer.PeerInfo.IsApplicationConnected = true; owner.Log("Application Connected: " + remotePeer.PeerInfo); owner.AddApplicationConnection(remotePeer); var listNotApplicationConnected = ListNotApplicationConnected(remotePeer.Connection); if (listNotApplicationConnected.Count > 0) { var notAppConnectedMessage = NetPeer.CreateMessage(); notAppConnectedMessage.Write(P2PServerMessage.PeerBecameApplicationConnected); notAppConnectedMessage.Write(remotePeer.PeerInfo.ConnectionId); NetPeer.SendMessage(notAppConnectedMessage, listNotApplicationConnected, NetDeliveryMethod.ReliableOrdered, 0); } var joinMessage = NetPeer.CreateMessage(); joinMessage.Write(P2PServerMessage.PeerBecameApplicationConnected); joinMessage.Write(remotePeer.PeerInfo.ConnectionId); var connectedMessage = NetPeer.CreateMessage(); connectedMessage.Write(P2PServerMessage.PeerBecameApplicationConnected); connectedMessage.Write(remotePeer.PeerInfo.ConnectionId); owner.NetworkApplication.JoinOnServer(remotePeer, joinMessage, connectedMessage); NetPeer.SendMessage(joinMessage, ListApplicationConnected(remotePeer.Connection), NetDeliveryMethod.ReliableOrdered, 0); NetPeer.SendMessage(connectedMessage, remotePeer.Connection, NetDeliveryMethod.ReliableOrdered, 0); }
void BecomeApplicationDisconnected(RemotePeer remotePeer) { Debug.Assert(remotePeer.PeerInfo.IsApplicationConnected); remotePeer.PeerInfo.IsApplicationConnected = false; owner.Log("Application Disconnected: " + remotePeer.PeerInfo); owner.RemoveApplicationConnection(remotePeer); var listNotApplicationConnected = ListNotApplicationConnected(remotePeer.Connection); if (listNotApplicationConnected.Count > 0) { var notAppConnectedMessage = NetPeer.CreateMessage(); notAppConnectedMessage.Write(P2PServerMessage.PeerLeftNetwork); notAppConnectedMessage.Write(remotePeer.PeerInfo.ConnectionId); NetPeer.SendMessage(notAppConnectedMessage, listNotApplicationConnected, NetDeliveryMethod.ReliableOrdered, 0); } var message = NetPeer.CreateMessage(); message.Write(P2PServerMessage.PeerLeftNetwork); message.Write(remotePeer.PeerInfo.ConnectionId); owner.NetworkApplication.LeaveOnServer(remotePeer, message); NetPeer.SendMessage(message, ListApplicationConnected(remotePeer.Connection), NetDeliveryMethod.ReliableOrdered, 0); }
public UnconnectedRemotePeer(P2PClient owner, RemotePeer remotePeer, int connectionToken, bool initiateConnection) { this.owner = owner; this.remotePeer = remotePeer; this.connectionToken = connectionToken; this.initiateConnection = initiateConnection; Debug.Assert(!remotePeer.IsConnected); // <- our job is to wait until we get this connection // Timing: double now = NetTime.Now; nextTick = now + tickRate; // The time-out times are calculated in the design document ("P2P Network.pptx") // They are dependent on the current Lidgren config, some Lidgren internals, and the punch-through settings (below) if (initiateConnection) { timeOutAt = now + 14; // seconds to receive punch-through message from other end so we can start initiating connection } else { timeOutAt = now + 32; // seconds to receive "Connect" (as "Accept") from other end initiating connection } // Send first punch-through: SendNatPunchThrough(); }
private const double pendingConnectionTimeout = 60; // seconds (calculated in "P2P Network.pptx") private void StartPendingConnection(RemotePeer remotePeer) { Debug.Assert(!pendingConnections.ContainsKey(remotePeer.PeerInfo.ConnectionId)); pendingConnections.Add(remotePeer.PeerInfo.ConnectionId, new PendingConnection(NetTime.Now + pendingConnectionTimeout, remotePeer)); }
internal void RemoveApplicationConnection(RemotePeer remotePeer) { Debug.Assert(remotePeerList.Contains(remotePeer)); remotePeerList.Remove(remotePeer); broadcastList.Remove(remotePeer.Connection); }
private void NetworkDataError(RemotePeer remotePeer, Exception exception) { owner.Log("Network data error from " + (remotePeer != null ? remotePeer.PeerInfo.ToString() : "unknown peer")); owner.LogException(exception); if (remotePeer != null) { Kick(remotePeer); // buh-bye } }
public void HandleMessage(NetIncomingMessage message, ref bool recycle) { Debug.Assert(!didDisconnect); RemotePeer senderRemotePeer = null; if (message.SenderConnection != null) { senderRemotePeer = message.SenderConnection.Tag as RemotePeer; } try { switch (message.MessageType) { case NetIncomingMessageType.DiscoveryRequest: // (Clients trying to find LAN games) { var response = NetPeer.CreateMessage(); DiscoveredGame.WriteDiscoveryResponse(owner.appConfig, response, owner.GameInfo, IsFull, owner.NetworkApplication.GetDiscoveryData()); NetPeer.SendDiscoveryResponse(response, message.SenderEndPoint); } break; case NetIncomingMessageType.StatusChanged: var status = (NetConnectionStatus)message.ReadByte(); switch (status) { case NetConnectionStatus.Connected: HandleConnection(message.SenderConnection); break; case NetConnectionStatus.Disconnected: HandleDisconnection(message.SenderConnection, message); break; } break; case NetIncomingMessageType.UnconnectedData: HandleUnconnectedMessage(message); break; case NetIncomingMessageType.Data: HandleNetworkManagementFromClient(message); break; } } catch (NetworkDataException exception) { NetworkDataError(senderRemotePeer, exception); } }
internal void AddApplicationConnection(RemotePeer remotePeer) { Debug.Assert(remotePeer.PeerInfo.IsApplicationConnected); Debug.Assert(!remotePeerList.Contains(remotePeer)); Debug.Assert(!broadcastList.Contains(remotePeer.Connection)); remotePeerList.Add(remotePeer); if (remotePeer.Connection != null) { broadcastList.Add(remotePeer.Connection); } }
private RemotePeer FindRemotePeerBySideChannelId(ulong sideChannelId) { foreach (NetConnection connection in locallyConnected) { RemotePeer remotePeer = connection.Tag as RemotePeer; Debug.Assert(remotePeer != null); if (remotePeer.PeerInfo.SideChannelId == sideChannelId) { return(remotePeer); } } return(null); }
private RemotePeer FindRemotePeerById(int connectionId) { foreach (NetConnection connection in locallyConnected) { RemotePeer remotePeer = connection.Tag as RemotePeer; Debug.Assert(remotePeer != null); if (remotePeer.PeerInfo.ConnectionId == connectionId) { return(remotePeer); } } return(null); }
private void HandleNetworkManagementFromClient(NetIncomingMessage message) { if (message.SenderConnection == null) { Debug.Assert(false); // This should never happen, unless Lidgren does something weird return; } RemotePeer remotePeer = message.SenderConnection.Tag as RemotePeer; if (remotePeer == null) { // Should never get here... unless Lidgren is being dumb (race condition?). At least assert our own state: Debug.Assert(!locallyConnected.Contains(message.SenderConnection)); return; } if (message.DeliveryMethod != NetDeliveryMethod.ReliableOrdered || message.SequenceChannel != 0) { Debug.Assert(false); // <- Message should have been handled elsewhere return; } var messageType = message.TryReadP2PClientMessage(); switch (messageType) { case P2PClientMessage.Connected: HandleReportConnected(message); break; case P2PClientMessage.Disconnected: HandleReportDisconnected(message); break; case P2PClientMessage.DisputeDisconnection: HandleReportDisputeDisconnection(message); break; case P2PClientMessage.RTTInitialised: MarkRemoteInitialisedRTT(remotePeer); break; case P2PClientMessage.AcceptHostMigration: AcceptHostMigration(remotePeer); break; default: throw new ProtocolException("Bad network client message: " + messageType); } }
void MarkRemoteInitialisedRTT(RemotePeer remotePeer) { PendingConnection pc; if (pendingConnections.TryGetValue(remotePeer.PeerInfo.ConnectionId, out pc)) { pc.remoteInitialisedRTT = true; ReadyCheck(pc); } else { throw new ProtocolException("Unexpected RTT initialised report from " + remotePeer.PeerInfo); } }
public void Kick(RemotePeer remotePeer) { Debug.Assert(remotePeer.IsConnected == locallyConnected.Contains(remotePeer.Connection)); if (!remotePeer.IsConnected) { return; // Already removed } owner.Log("Kicking " + remotePeer.PeerInfo.ToString()); locallyConnected.Remove(remotePeer.Connection); remotePeer.Disconnect(DisconnectStrings.Kicked); RemoveFromNetwork(remotePeer); }
private void UpdatePendingDisconnects(double currentTime) { double now = NetTime.Now; // Only take things from the front of the queue (because when something gets kicked, the pendingDisconnects list can change) while (pendingDisconnects.Count > 0 && pendingDisconnects[0].timeOut < currentTime) { // Timed out: PendingDisconnect pd = pendingDisconnects[0]; RemotePeer remotePeer = FindRemotePeerById(pd.failedConnectTo); owner.Log("Timed out after disconnect: " + remotePeer.PeerInfo.ToString()); Kick(remotePeer); } }
private void HandleReportDisconnected(NetIncomingMessage message) { RemotePeer reporter = (message.SenderConnection.Tag as RemotePeer); int disconnectedId; try { disconnectedId = message.ReadInt32(); } catch (Exception e) { throw new ProtocolException("Bad disconnected report", e); } RemotePeer disconnected = FindRemotePeerById(disconnectedId); if (disconnected == null) { return; // already disconnected (or bad ID) } // The network trust model says that we can't tell which client is misbehaving (or has a misbehaving link). // If one claims to be disconnected from another, unless we involve other clients (over-complicated), we can't tell who is misbehaving. // The network requires all clients are connected. So we just have to pick one to disconnect. // Prefer to disconnect the "newer" client. But if they reported first, check whether the older client is actually alive. if (reporter.PeerInfo.ConnectionId < disconnectedId) // Older client disconnected from newer client { owner.Log(disconnected.PeerInfo + " was disconnected by " + reporter.PeerInfo); Kick(disconnected); } else if (reporter.PeerInfo.ConnectionId == disconnectedId) // They disconnected themselves! (oooookay...) { owner.Log(reporter.PeerInfo + " disconnected from self (weird...)"); Kick(reporter); // You're the boss... } else // Newer client disconnected from older client { owner.Log(disconnected.PeerInfo + " was disconnected by " + reporter.PeerInfo + " and has a chance to dispute."); // Inform the older client: var msg = NetPeer.CreateMessage(); msg.Write(P2PServerMessage.YouWereDisconnectedBy); msg.Write(reporter.PeerInfo.ConnectionId); disconnected.Connection.SendMessage(msg, NetDeliveryMethod.ReliableOrdered, 0); // And wait for them to respond: AddPendingDisconnect(reporter.PeerInfo.ConnectionId, disconnectedId); } }
/// <summary>Call this after host migration, after re-issuing adding authorised users from the side-channel</summary> internal void SideChannelValidateAndKick() { if (!owner.GameInfo.SideChannelAuth) { owner.Log("ERROR: SideChannelValidateAndKick in non-side-channel game"); return; } foreach (NetConnection connection in locallyConnected) { RemotePeer remotePeer = connection.Tag as RemotePeer; Debug.Assert(remotePeer != null); if (authTokens == null || !authTokens.ContainsKey(remotePeer.PeerInfo.SideChannelId)) { Kick(remotePeer); } } }
private void HandleReportConnected(NetIncomingMessage message) { RemotePeer reporter = (message.SenderConnection.Tag as RemotePeer); int connectedId; try { connectedId = message.ReadInt32(); } catch (Exception e) { throw new ProtocolException("Bad connected report", e); } RemotePeer connected = FindRemotePeerById(connectedId); if (connected == null) { return; // was disconnected (or bad ID) } owner.Log(reporter.PeerInfo.ToString() + " connected to " + connected.PeerInfo.ToString()); MarkConnectionMade(reporter.PeerInfo.ConnectionId, connectedId); }
private void RemoveFromNetwork(RemotePeer remotePeer) { ReleaseInputAssignment(remotePeer.PeerInfo.InputAssignment); // Clean up pending: RemovePendingDisconnectsFor(remotePeer.PeerInfo.ConnectionId); RemovePendingConnectionsFor(remotePeer.PeerInfo.ConnectionId); if (!remotePeer.PeerInfo.IsApplicationConnected) { // Wasn't application connected yet (just send the notification) var message = NetPeer.CreateMessage(); message.Write(P2PServerMessage.PeerLeftNetwork); message.Write(remotePeer.PeerInfo.ConnectionId); NetPeer.SendMessage(message, locallyConnected, NetDeliveryMethod.ReliableOrdered, 0); } else { BecomeApplicationDisconnected(remotePeer); } // Possible that connecting clients no longer need to connect to this one: ReadyCheckAll(); }
private void HandleDisconnection(NetConnection connection, NetIncomingMessage message) { RemotePeer remotePeer = connection.Tag as RemotePeer; if (remotePeer == null) { Debug.Assert(!locallyConnected.Contains(connection)); return; // Already dealt with } Debug.Assert(remotePeer.Connection == connection); Debug.Assert(locallyConnected.Contains(remotePeer.Connection)); string reason = message.ReadString(); owner.Log("Disconnected: " + remotePeer.PeerInfo + " (supplied reason: " + reason + ")"); locallyConnected.Remove(remotePeer.Connection); remotePeer.WasDisconnected(); RemoveFromNetwork(remotePeer); CheckDisconnectionForHostMigrationInvalidation(reason); }
/// <summary> /// Update network state, and poll for and handle messages. /// </summary> public void Update() { try { // Kick anyone who was queued up to be kicked if (peerManager != null) { foreach (var remotePeer in networkDataErrorKickList) { peerManager.KickDueToNetworkDataError(remotePeer); } } networkDataErrorKickList.Clear(); NetIncomingMessage message; while ((message = netPeer.ReadMessage()) != null) { bool recycle = true; LogNetMessage(message); switch (message.MessageType) { // Discovery: case NetIncomingMessageType.DiscoveryResponse: if (Discovery != null) { Discovery.ReceiveDiscoveryResponse(message); } break; // Endpoint requests case NetIncomingMessageType.EndpointRequestResult: if (OnExternalEndpointDiscovered != null) { OnExternalEndpointDiscovered(message.SenderEndPoint); } break; // Application-layer data: case NetIncomingMessageType.Data: if (message.DeliveryMethod == NetDeliveryMethod.ReliableOrdered && message.SequenceChannel == 0) // ... except network management { if (peerManager != null) { peerManager.HandleMessage(message, ref recycle); } } else { RemotePeer remotePeer = message.SenderConnection.Tag as RemotePeer; if (remotePeer != null) { remotePeer.QueueMessage(message, ref recycle); } else { // Warn if an unknown peer sends us anything (unless it's lidgren being dumb and giving us data for a connection they told us disconnected) Debug.Assert(message.SenderConnection.Status == NetConnectionStatus.Disconnected || message.SenderConnection.Status == NetConnectionStatus.Disconnecting); } } break; // All other messages are presumed to be for the peer manager (or garbage): default: if (peerManager != null) { peerManager.HandleMessage(message, ref recycle); } break; } if (recycle) { netPeer.Recycle(message); } } if (peerManager != null) { peerManager.Update(); } } catch (NetworkDisconnectionException) { Debug.Assert(peerManager == null); // <- we did actually get disconnected, right? } if (Discovery != null) { Discovery.Update(); } }
void AcceptHostMigration(RemotePeer remotePeer) { owner.Log("Host migration accepted by: " + remotePeer.PeerInfo); ValidateHostMigration(); }
public void KickDueToNetworkDataError(RemotePeer remotePeer) { Debug.Assert(remotePeer != null); Kick(remotePeer); // buh-bye }
private void HandleConnection(NetConnection connection) { // Read hail message: IPEndPoint internalEndPoint; ulong sideChannelId; int sideChannelToken; string requestedName; byte[] remotePlayerData = null; try { UInt16 theirAppVersion = connection.RemoteHailMessage.ReadUInt16(); if (theirAppVersion != owner.appConfig.ApplicationVersion) { owner.Log("Disconnected client with wrong application version"); connection.Disconnect(DisconnectStrings.BadGameVersion); return; } int theirAppSignatureLength = (int)connection.RemoteHailMessage.ReadVariableUInt32(); byte[] theirAppSignature; if (theirAppSignatureLength < 0 || theirAppSignatureLength > NetworkAppConfig.ApplicationSignatureMaximumLength) { theirAppSignature = null; } else { theirAppSignature = connection.RemoteHailMessage.ReadBytes(theirAppSignatureLength); } if (theirAppSignature == null || !owner.appConfig.ApplicationSignature.SequenceEqual(theirAppSignature)) { owner.Log("Disconnected client with wrong application signature"); connection.Disconnect(DisconnectStrings.BadGameVersion); return; } internalEndPoint = connection.RemoteHailMessage.ReadIPEndPoint(); sideChannelId = connection.RemoteHailMessage.ReadUInt64(); sideChannelToken = connection.RemoteHailMessage.ReadInt32(); requestedName = connection.RemoteHailMessage.ReadString(); remotePlayerData = connection.RemoteHailMessage.ReadByteArray(); } catch { owner.UnconnectedProtocolError("Bad hail message"); connection.Disconnect(DisconnectStrings.BadHailMessage); return; } if (owner.GameInfo.SideChannelAuth) { int expectedToken; if (sideChannelToken == 0 || authTokens == null || !authTokens.TryGetValue(sideChannelId, out expectedToken) || expectedToken != sideChannelToken) { owner.UnconnectedProtocolError("Bad side-channel authentication"); connection.Disconnect(DisconnectStrings.BadSideChannelAuth); } } requestedName = requestedName.FilterNameNoDuplicates(locallyConnected, owner.LocalPeerInfo.PlayerName); // Check whether the server is full: // (Could possibly handle this as Approve/Deny, but would then have to track slots for pending connections.) if (IsFull) { owner.Log("Rejecting incoming connection (player name: " + requestedName + "): game is full"); connection.Disconnect(DisconnectStrings.GameFull); return; } // Setup remote peer: PeerInfo peerInfo = new PeerInfo() { ConnectionId = nextConnectionId++, PlayerName = requestedName, PlayerData = remotePlayerData, InputAssignment = AssignInput(), InternalEndPoint = internalEndPoint, ExternalEndPoint = connection.RemoteEndPoint, SideChannelId = sideChannelId, IsApplicationConnected = false, IsServer = false, }; RemotePeer remotePeer = new RemotePeer(connection, peerInfo); owner.Log("New connection: " + peerInfo.ToString()); // Setup connection tokens: int[] connectionTokens = new int[locallyConnected.Count]; for (int i = 0; i < connectionTokens.Length; i++) { connectionTokens[i] = random.Next(); } // Send info to joining peer: { var message = NetPeer.CreateMessage(); message.Write(P2PServerMessage.NetworkStartInfo); owner.GameInfo.WriteTo(message); peerInfo.WriteTo(message, true); // Send their peer info (except: they already have their own playerData) owner.LocalPeerInfo.WriteTo(message); // Send our peer info // Write out a list of who to connect to: Debug.Assert(locallyConnected.Count < byte.MaxValue); message.Write((byte)(locallyConnected.Count)); for (int i = 0; i < locallyConnected.Count; i++) { (locallyConnected[i].Tag as RemotePeer).PeerInfo.WriteTo(message); message.Write(connectionTokens[i]); } remotePeer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 0); } // Send connect instructions to other peers: for (int i = 0; i < locallyConnected.Count; i++) { var message = NetPeer.CreateMessage(); message.Write(P2PServerMessage.PeerJoinedNetwork); peerInfo.WriteTo(message); message.Write(connectionTokens[i]); locallyConnected[i].SendMessage(message, NetDeliveryMethod.ReliableOrdered, 0); } // Add new connection to the local connection list: locallyConnected.Add(connection); // Start waiting for this new client to connect to the full network: StartPendingConnection(remotePeer); // Although the first connection won't need to wait: if (locallyConnected.Count == 1) { ReadyCheckAll(); } }