Results 1 to 15 of 15
Like Tree5Likes
  • 2 Post By twostars
  • 1 Post By twostars
  • 1 Post By twostars
  • 1 Post By twostars

Cleaning up official 1.298 server socket behaviour (or: why NOT to use this server)

This is a discussion on Cleaning up official 1.298 server socket behaviour (or: why NOT to use this server) within the Private Server Technical Support forums, part of the Private Servers category; So I had the brief pleasure of looking at the Global-MYKO server while it was experiencing attacks. Nothing new - ...
Page: 1


  1. #1
    Senior Member
    Join Date
    Dec 2009
    Posts
    1,805

    Default Cleaning up official 1.298 server socket behaviour (or: why NOT to use this server)

    So I had the brief pleasure of looking at the Global-MYKO server while it was experiencing attacks.
    Nothing new - people flooding sockets and the like. People have done this since forever; it's been at least 5 or 6 years since I last had to deal with this (since, obviously I've moved over to my own server files and never had to worry about any of this again).

    They, like virtually anyone using the official 1.298 files at this point are using SOACS. Unfortunately, they couldn't get ahold of osmanx in a timely manner so they resorted to contacting me (as a final last resort).
    Contacting me apparently didn't fly well with osmanx, as he's no longer supporting them because of this. Ouch.

    Anyway, back to the real story here -- so essentially the way the server works is it allocates 1500 sessions and 1500 session IDs. The session IDs are stored in a list, which it'll take from/restore as sessions are connected or disconnect.
    In addition to the obvious denial of service from filling up the sockets, the server would start losing available session IDs (despite there being no sessions connected to use them).
    Meaning that eventually (with the attacks, pretty quickly actually), it would stop accepting connections altogether because there's no session IDs to pull from -- yet the connections themselves are already disconnected. Those session IDs are just lost to the world. This is just the beginning of the WTF-ery; it actually gets worse when looking into it, as I'll soon explain.

    At this point in time you'd expect the server not to have any basic issues with handling sockets/sessions, but apparently this isn't the case.

    So, he reduces the rate of this problem by enforcing a whitelist (which is perfectly fine by itself, but not to specifically treat this issue) but that doesn't really address it so much as it deters it. Additionally, he's clearly experienced the crashes from it as for every related pointer check he's added checks to verify each session doesn't point to a known bad debugger value (e.g. 0xfeeefeee / 0xdddddddd for freed/deleted pointers, etc.). The 1.298 server files were built in debug mode, so it will use these -- but they shouldn't be relied on. If we're at this point where the session pointer is broken, it means things have already gone horribly wrong, and it's not just these pointers that can break here.

    Checking these merely masks the problem.

    So what *is* the problem?

    Let's take a look at how the official code works (SOACS logic is slightly different, but it merely just adds the various pointer checks -- so we don't care about that).

    The server has 1 thread for accepting connections. This is called AcceptThread.
    Then it has multiple threads for handling packets/disconnects. These are called ReceiveWorkerThread.
    Finally it has multiple threads for handling AI connections. These are called ClientWorkerThread.

    The latter is worth noting for future because it's very similar to ReceiveWorkerThread (for player connections), however AI sessions do not use session IDs.

    So let's start by looking at AcceptThread, i.e. when a new connection is handled.
    In the client source, this is a fair bit cleaner/easier to understand, and for this example is identical, so let's look at that:
    Code:
    DWORD WINAPI AcceptThread(LPVOID lp)
    {
    	CIOCPort* pIocport = (CIOCPort*) lp;
    
    	WSANETWORKEVENTS	network_event;
    	DWORD				wait_return;
    	int					sid;
    	CIOCPSocket2*		pSocket = NULL;
    	char				logstr[1024];
    	memset( logstr, NULL, 1024 );
    
    	struct sockaddr_in  addr;
    	int					len;
    
    	while(1)
    	{
    		wait_return = WaitForSingleObject( pIocport->m_hListenEvent, INFINITE);
    
    		if(wait_return == WAIT_FAILED)
    		{
    			TRACE("Wait failed Error %d\n", GetLastError());
    			char logstr[1024]; memset( logstr, NULL, 1024 );
    			sprintf( logstr, "Wait failed Error %d\r\n", GetLastError());
    			LogFileWrite( logstr );
    			return 1;
    		}
    
    		WSAEnumNetworkEvents( pIocport->m_ListenSocket, pIocport->m_hListenEvent, &network_event);
    
    		if(network_event.lNetworkEvents &FD_ACCEPT)
    		{
    			if(network_event.iErrorCode[FD_ACCEPT_BIT] == 0 ) 
    			{
    				sid = pIocport->GetNewSid();
    				if(sid < 0) {
    					TRACE("Accepting User Socket Fail - New Uid is -1\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Accepting User Socket Fail - New Uid is -1\r\n");
    					LogFileWrite( logstr );
    					goto loop_pass_accept;
    				}
    
    				pSocket = pIocport->GetIOCPSocket( sid );
    				if( !pSocket ) {
    					TRACE("Socket Array has Broken...\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Socket Array has Broken...\r\n");
    					LogFileWrite( logstr );
    					goto loop_pass_accept;
    				}
    
    				len = sizeof(addr);
    				if( !pSocket->Accept( pIocport->m_ListenSocket, (struct sockaddr *)&addr, &len ) ) {
    					TRACE("Accept Fail %d\n", sid);
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Accept Fail %d\r\n", sid);
    					LogFileWrite( logstr );
    					pIocport->RidIOCPSocket( sid, pSocket );
    					pIocport->PutOldSid( sid );
    					goto loop_pass_accept;
    				}
    
    				pSocket->InitSocket( pIocport );
    
    				if( !pIocport->Associate( pSocket, pIocport->m_hServerIOCPort ) ) {
    					TRACE("Socket Associate Fail\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Socket Associate Fail\r\n");
    					LogFileWrite( logstr );
    					pSocket->CloseProcess();
    					pIocport->RidIOCPSocket( sid, pSocket );
    					pIocport->PutOldSid( sid );
    					goto loop_pass_accept;
    				}
    
    				pSocket->Receive();
    			}
    
    loop_pass_accept:
    			continue;
    		}
    	}
    	
    	return 1;
    }
    Stepping through it from the time a connection is detected:
    Code:
    				sid = pIocport->GetNewSid();
    				if(sid < 0) {
    					TRACE("Accepting User Socket Fail - New Uid is -1\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Accepting User Socket Fail - New Uid is -1\r\n");
    					LogFileWrite( logstr );
    					goto loop_pass_accept;
    				}
    The first thing it'll do when it detects a new connection is it'll call CIOCPort::GetNewSid().
    The intention of this is to retrieve the next available session ID to use for this session and remove it from the pool.
    Note: This happens before we've even accepted the connection. This is important to note because soacs' internal whitelist hooks the accept() call. We have not accepted the connection yet, but we have allocated an ID for it.

    We'll get into this more later, but for now CIOCPort::GetNewSid() does this:
    Code:
    signed int __thiscall CIOCPort::GetNewSid(CIOCPort *this)
    {
      signed int result; // eax
      signed int ret; // esi
    
      if ( !this->m_SidList.empty() )
      {
        ret = m_SidList.front();
        m_SidList.pop_front(); // std::list<int,std::allocator<int>>::pop_front(&this->m_SidList);
        result = ret;
      }
      else
      {
        CIOCPort::RefreshSidList(this);
        result = -1;
      }
      return result;
    }
    Or cleaned up some more, it'd look like this in the original source (what's important to note is that in the source we have, RefreshSidList() does not exist):
    Code:
    int CIOCPort::GetNewSid()
    {
      if (m_SidList.empty())
      {
         RefreshSidList();
         return -1;
      }
    
      int ret = m_SidList.front();
      m_SidList.pop_front();
      return ret;
    }
    So, if the list is empty call RefreshSidList(), the purpose of which is to try and fix desync issues (yes, rather than fix the problem, they wrote this to try and deal with it when it happened) by reclaiming session IDs for unused sessions. There's a whole level of WTF involved here because it's mgame, so obviously they never even got this much right -- and in fact it makes the problem *worse*. But I'll get into that after.
    After it calls RefreshSidList() it will return -1 to indicate it did not find a session ID to use.

    If the list contains session IDs, we'll store the first session ID in the list, remove this from the pool and then return it to AcceptThread.

    Back to AcceptThread:
    Code:
    				if(sid < 0) {
    					TRACE("Accepting User Socket Fail - New Uid is -1\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Accepting User Socket Fail - New Uid is -1\r\n");
    					LogFileWrite( logstr );
    					goto loop_pass_accept;
    				}
    So if it fails and didn't find a session ID, we'll log that and then proceed to waiting for & handling the next connection event.

    When we do correctly get assigned a session ID however, our next step is associating that with a session (note: I'll likely interchangeably use socket and session to refer to the same thing - the allocated session which the server will assign the socket to).
    Code:
    				pSocket = pIocport->GetIOCPSocket( sid );
    				if( !pSocket ) {
    					TRACE("Socket Array has Broken...\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Socket Array has Broken...\r\n");
    					LogFileWrite( logstr );
    					goto loop_pass_accept;
    				}
    CIOCPSocket2::GetIOCPSocket() is also (almost) identical in the client source.
    I've tweaked the source below to match it -- the only change here is a compiler optimisation with it storing m_SockArrayInActive[index] and checking against the stored value, instead of checking m_SockArrayInActive[index] both times.
    I note this because it's actually a very important change in the event of race conditions, which I'll get to in a bit.
    Code:
    CIOCPSocket2* CIOCPort::GetIOCPSocket(int index)
    {
    	if( index > m_SocketArraySize ) {
    		TRACE("InActiveSocket Array Overflow[%d]\n", index );
    		return NULL;
    	}
    
    	CIOCPSocket2 *pIOCPSock = (CIOCPSocket2 *)m_SockArrayInActive[index];
    	if ( pIOCPSock ) {
    		TRACE("InActiveSocket Array Invalid[%d]\n", index );
    		return NULL;
    	}
    
    	m_SockArray[index] = pIOCPSock;
    	m_SockArrayInActive[index] = NULL;
    
    	pIOCPSock->SetSocketID( index );
    
    	return pIOCPSock;
    }
    In other words, it will first check if the session ID is even valid.
    If it's valid, it'll pull the session from its inactive pool. It's important to note (again, for later) that rather than an actual pool, each session is mapped 1:1 with a session ID, so session ID 0 will always point to the first session in the array.
    It'll then move the session into its corresponding slot in the active pool, attach the session ID to the session (pretty pointless considering it's never going to be mapped to a different session but oh well), and finally return that session to AcceptThread.

    Code:
    				if( !pSocket ) {
    					TRACE("Socket Array has Broken...\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Socket Array has Broken...\r\n");
    					LogFileWrite( logstr );
    					goto loop_pass_accept;
    				}
    If no session was found, it'll log this and continue on.
    Note that it won't return the session ID to the pool at this point.
    While it "should", it'll only fail if the session is in use (session ID already in use, which actually CAN happen) or the session ID is just wrong (should never happen). In these cases, there's not actually much point to restoring it as it will just keep using this broken session and failing to accept these connections until the end of time. So this is actually preemptively handling these potential (actually occurring) issues by sheer luck really. Again, I'll get more onto these issues in a bit.

    So we're assigned a session ID, and we have a session to go with it. Now we can accept the connection.
    Code:
    				len = sizeof(addr);
    				if( !pSocket->Accept( pIocport->m_ListenSocket, (struct sockaddr *)&addr, &len ) ) {
    					TRACE("Accept Fail %d\n", sid);
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Accept Fail %d\r\n", sid);
    					LogFileWrite( logstr );
    					pIocport->RidIOCPSocket( sid, pSocket );
    					pIocport->PutOldSid( sid );
    					goto loop_pass_accept;
    				}
    At this point we call CIOCPSocket2::Accept(), which looks like this:
    Code:
    BOOL CIOCPSocket2::Accept( SOCKET listensocket, struct sockaddr* addr, int* len )
    {
    	m_Socket = accept( listensocket, addr, len);
    	if( m_Socket == INVALID_SOCKET) {
    		int err = WSAGetLastError();
    		TRACE("Socket Accepting Fail - %d\n", err);
    		char logstr[1024]; memset( logstr, NULL, 1024 );
    		sprintf( logstr, "Socket Accepting Fail - %d\r\n", err);
    		LogFileWrite( logstr );
    		return FALSE;
    	}
    
    	struct linger lingerOpt;
    
    	lingerOpt.l_onoff = 1;
    	lingerOpt.l_linger = 0;
    
    	setsockopt(m_Socket, SOL_SOCKET, SO_LINGER, (char *)&lingerOpt, sizeof(lingerOpt));
    
    	return TRUE;
    }
    So this calls accept() (note: the soacs whitelist applies here) to accept the connection. If it failed, it'll log & return false.
    If it succeeded, it'll set the linger timeout (more info https://stackoverflow.com/questions/...n-its-required) and return true.

    Here's where things start getting fun. If we failed to accept the connection (e.g. because soacs blocked it):
    Code:
    				if( !pSocket->Accept( pIocport->m_ListenSocket, (struct sockaddr *)&addr, &len ) ) {
    					TRACE("Accept Fail %d\n", sid);
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Accept Fail %d\r\n", sid);
    					LogFileWrite( logstr );
    					pIocport->RidIOCPSocket( sid, pSocket );
    					pIocport->PutOldSid( sid );
    					goto loop_pass_accept;
    				}
    It will log this, call CIOCPort::RidIOCPSocket() (restore the session back to the inactive session pool) and CIOPort::PutOldSid() (restore the session ID back to the pool), then wait for the next new connection event.

    CIOCPort::RidIOCPSocket():
    Code:
    void CIOCPort::RidIOCPSocket(int index, CIOCPSocket2 *pSock)
    {
    	if( index < 0 || (pSock->GetSockType() == TYPE_ACCEPT && index >= m_SocketArraySize) || (pSock->GetSockType() == TYPE_CONNECT && index >= m_ClientSockSize) ) {
    		TRACE("Invalid Sock index - RidIOCPSocket\n");
    		return;
    	}
    	if( pSock->GetSockType() == TYPE_ACCEPT ) {
    		m_SockArray[index] = NULL;
    		m_SockArrayInActive[index] = pSock;
    	}
    	else if( pSock->GetSockType() == TYPE_CONNECT ){
    		m_ClientSockArray[index] = NULL;
    	}
    }
    (we only care about TYPE_ACCEPT sockets -- these are client sockets. TYPE_CONNECT are AI server sockets, as Ebenezer is making a connection to AI server as opposed to accepting a connection from the client)

    CIOCPort::PutOldSid():
    Code:
    void CIOCPort::PutOldSid(int sid)
    {
    	if( sid < 0 || sid > m_SocketArraySize ) {
    		TRACE("recycle sid invalid value : %d\n", sid);
    		return;
    	}
    
    	list<int>::iterator  Iter;
    	Iter = find( m_SidList.begin(), m_SidList.end(), sid );
    	if( Iter != m_SidList.end() )
    		return;
    	
    	m_SidList.push_back(sid);
    }
    This checks if the session ID is valid and doesn't already exist in the list. If not, it'll add it back to the end of the list.
    Last edited by twostars; 08-29-2018 at 02:53 PM.
    Kallop and l911l like this.

  2. #2
    Senior Member
    Join Date
    Dec 2009
    Posts
    1,805

    Default

    Assuming it succeeded, all is pretty much fine here so far (or is it? ^_^).
    Code:
    				pSocket->InitSocket( pIocport );
    
    				if( !pIocport->Associate( pSocket, pIocport->m_hServerIOCPort ) ) {
    					TRACE("Socket Associate Fail\n");
    					char logstr[1024]; memset( logstr, NULL, 1024 );
    					sprintf( logstr, "Socket Associate Fail\r\n");
    					LogFileWrite( logstr );
    					pSocket->CloseProcess();
    					pIocport->RidIOCPSocket( sid, pSocket );
    					pIocport->PutOldSid( sid );
    					goto loop_pass_accept;
    				}
    
    				pSocket->Receive();
    So it'll initialize the session and create an i/o completion port for it, which is essentially tying it to a ReceiveWorkerThread for further handling.
    Finally, it'll trigger the first read on this socket in this thread before proceeding to the next new connection event.

    So that's AcceptThread. To recap, when all is going well it will:
    - New connection is received
    - Allocate a session ID for this connection
    - Fetch the associated session for this session ID
    - Accept the connection
    - Initialize the session
    - Enable further reading for this session via ReceiveWorkerThread
    - Read the first data sent

    By itself, no problems here. It's a single thread, so no problems with the session ID pool etc.

    But what about when ReceiveWorkerThread becomes involved?
    ReceiveWorkerThread is also identical to the source. So again, for clarity let's refer to that:
    Code:
    DWORD WINAPI ReceiveWorkerThread(LPVOID lp)
    {
    	CIOCPort* pIocport = (CIOCPort*) lp;
    
    	DWORD			WorkIndex;	
    	BOOL			b;
    	LPOVERLAPPED	pOvl;
    	DWORD			nbytes;
    	DWORD			dwFlag = 0;
    	CIOCPSocket2*	pSocket = NULL;
    
    	while (1)
    	{
    		b = GetQueuedCompletionStatus( 
    									  pIocport->m_hServerIOCPort,
    									  &nbytes,
    									  &WorkIndex,
    									  &pOvl,
    									  INFINITE);
    		if(b || pOvl) 
    		{
    			if(b)
    			{
    				if( WorkIndex > (DWORD)pIocport->m_SocketArraySize )
    					goto loop_pass;
    				pSocket = (CIOCPSocket2 *)pIocport->m_SockArray[WorkIndex];
    				if( !pSocket )
    					goto loop_pass;
    
    				switch( pOvl->Offset )
    				{
    				case	OVL_RECEIVE:
    					EnterCriticalSection( &g_critical );
    					if( !nbytes ) {
    						TRACE("User Closed By 0 byte Notify...%d\n", WorkIndex);
    						pSocket->CloseProcess();
    						pIocport->RidIOCPSocket( pSocket->GetSocketID(), pSocket );
    						pIocport->PutOldSid( pSocket->GetSocketID() );
    						LeaveCriticalSection( &g_critical );
    						break;
    					}
    
    					pSocket->m_nPending = 0;
    					pSocket->m_nWouldblock = 0;
    
    					pSocket->ReceivedData((int)nbytes);
    					pSocket->Receive();
    					LeaveCriticalSection( &g_critical );
    					break;
    				case	OVL_SEND:
    					pSocket->m_nPending = 0;
    					pSocket->m_nWouldblock = 0;
    
    					break;
    				case	OVL_CLOSE:
    					EnterCriticalSection( &g_critical );
    					TRACE("User Closed By Close()...%d\n", WorkIndex);
    
    					pSocket->CloseProcess();
    					pIocport->RidIOCPSocket( pSocket->GetSocketID(), pSocket );
    					pIocport->PutOldSid( pSocket->GetSocketID() );
    
    					LeaveCriticalSection( &g_critical );
    					break;
    				default:
    					break;
    				}
    			}
    			else {
    				if( WorkIndex > (DWORD)pIocport->m_SocketArraySize )
    					goto loop_pass;
    				pSocket = (CIOCPSocket2 *)pIocport->m_SockArray[WorkIndex];
    				if( !pSocket )
    					goto loop_pass;
    
    				EnterCriticalSection( &g_critical );
    
    				pSocket->CloseProcess();
    				pIocport->RidIOCPSocket( pSocket->GetSocketID(), pSocket );
    				pIocport->PutOldSid( pSocket->GetSocketID() );
    				
    				LeaveCriticalSection( &g_critical );
    
    				if( pOvl ) {
    					TRACE("User Closed By Abnormal Termination...%d\n", WorkIndex);
    				}
    				else {
    					DWORD ioError = GetLastError();
    					TRACE("User Closed By IOCP Error[%d] - %d \n", ioError, WorkIndex);
    								
    				}
    			}
    		}
    
    loop_pass:
    		continue;
    	}
    
    	return 1;
    }
    It's not especially that complicated here either, so let's take a closer look at it

    To summarise, ReceiveWorkerThread is seeing events triggered when a player is disconnected (either via a read notification of 0 bytes from the client or a disconnect request from the server), we're receiving packets, or we're sending packets to the client and handling each event appropriately.

    Without getting into the specifics of this, with the way this works with Windows' completion ports, each session is essentially assigned to the same ReceiveWorkerThread for all of its processing. This is important to note because it means there's no risk of packets being handled out of order and such.

    There's 2 main parts to ReceiveWorkerThread. Either it can fetch the event status or it can't.
    If it can fetch the status, then it can determine which type of event occurred (read, send, disconnect).
    If it can't, the server has no choice but to disconnect them.

    Essentially both behave much the same, so for a failed status lookup it'll start by fetching the associated session for this session ID:
    Code:
    				if( WorkIndex > (DWORD)pIocport->m_SocketArraySize )
    					goto loop_pass;
    				pSocket = (CIOCPSocket2 *)pIocport->m_SockArray[WorkIndex];
    				if( !pSocket )
    					goto loop_pass;
    Code:
    				EnterCriticalSection( &g_critical );
    
    				pSocket->CloseProcess();
    				pIocport->RidIOCPSocket( pSocket->GetSocketID(), pSocket );
    				pIocport->PutOldSid( pSocket->GetSocketID() );
    				
    				LeaveCriticalSection( &g_critical );
    And then it'll do something we've yet to actually see: it'll guard the following logic with critical sections (i.e. a mutex), which causes any other threads calling EnterCriticalSection(&g_critical) to wait until it's done (i.e. LeaveEnterCriticalSection(&g_critical) is called).

    This is very, very important when dealing with shared data across multiple threads. Doing so prevents race conditions involving shared data.
    What is a race condition? StackOverflow is, as usual, extremely helpful - https://stackoverflow.com/questions/...race-condition

    These 2 answers sum it up pretty nicely:
    A race condition occurs when two or more threads can access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data. Therefore, the result of the change in data is dependent on the thread scheduling algorithm, i.e. both threads are "racing" to access/change the data.

    Problems often occur when one thread does a "check-then-act" (e.g. "check" if the value is X, then "act" to do something that depends on the value being X) and another thread does something to the value in between the "check" and the "act".
    Still don't follow? How about this excellent real world example:
    You are planning to go to a movie at 5 pm. You inquire about the availability of the tickets at 4 pm. The representative says that they are available. You relax and reach the ticket window 5 minutes before the show. I'm sure you can guess what happens: it's a full house. The problem here was in the duration between the check and the action. You inquired at 4 and acted at 5. In the meantime, someone else grabbed the tickets. That's a race condition - specifically a "check-then-act" scenario of race conditions.
    So now that we're familiar with what your typical race condition acts like, let's go back to analysing the code.

    While it's guarded, it runs this:
    Code:
    				pSocket->CloseProcess();
    				pIocport->RidIOCPSocket( pSocket->GetSocketID(), pSocket );
    				pIocport->PutOldSid( pSocket->GetSocketID() );
    CIOCPSocket2::CloseProcess() doesn't do a whole lot worth guarding, however RidIOCPSocket() and PutOldSid() on the other hand...

    As we showed before, they both modify the state of shared data that's used by this thread, AcceptThread and ClientWorkerThread (the AI equivalent, although technically AI touches a different array).

    CIOCPort::PutOldSid() in particular modifies m_SidList, which AcceptThread or another ReceiveWorkerThread could be doing concurrently, be it calling PutOldSid() to restore a socket, or calling GetNewSid() to remove one.

    If you note, all situations involving modifying these with ReceiveWorkerThread are guarded.
    AcceptThread, on the other hand, does no such thing.

    And therein lies the problem. While m_SidList is being touched in ReceiveWorkerThread (by players disconnecting), AcceptThread can also be touching it (as it's not guarded), causing unexpected results when a race condition occurs (i.e. they happen at the same time).
    With attacks, this problem becomes far more pronounced, because obviously there's a lot of connection attempts happening at once, vastly increasing the likelihood that both events will happen at the same time.
    Kallop likes this.

  3. #3
    Senior Member
    Join Date
    Dec 2009
    Posts
    1,805

    Default

    But an attack doesn't need to occur for this to happen. This can happen, 'at random', with regular activity. It's all about timing.

    Boiling the issue down, the only thing that actually needs to be guarded is CIOCPort::m_SidList (provided we handle restoration of the session ID [PutOldSid] after restoring the session to the pool [RidIOCPSocket], which is always the case). This is because sessions are mapped 1:1 with their sockets in an array, so if the session ID is in use, it won't be in the list for other things to worry about.

    So assuming there's no other nasty interactions here (there is though, which I'll mention in a sec), the solution here is to simply guard PutOldSid() and GetNewSid().
    I solved this with EbeXtender via detouring those 2 methods. I personally replaced them entirely as such:
    Code:
    static Mutex s_sidLock;
    static std::set<int> s_sidList;
    
    int CIOCPort::GetNewSid()
    {
    	Guard<Mutex> lock(s_sidLock);
    	auto itr = s_sidList.begin();
    	if (itr == s_sidList.end())
    		return -1;
    
    	int sid = *itr;
    	s_sidList.erase(itr);
    	return sid;
    }
    
    void CIOCPort::PutOldSid(int sid)
    {
    	Guard<Mutex> lock(s_sidLock);
    	s_sidList.insert(sid);
    }
    
    void CIOCPort::Init(int serversocksize, int clientsocksize, int workernum /*= 0*/)
    {
    	{
    		Guard<Mutex> lock(s_sidLock);
    		for (int i = 0; i < serversocksize; i++)
    			s_sidList.insert(i);
    	}
    
    	SETUP_THIS_ORIG(void, CIOCPort, Init, (int, int, int));
    	CALL_THIS_ORIG((serversocksize, clientsocksize, workernum));
    }
    I did this so it'd use std::set<> (IDs are guaranteed to be unique) instead of std::list<> (can contain duplicates), but you can simply wrap the existing calls in a mutex, e.g.:
    Code:
    int CIOCPort::GetNewSid()
    {
    	EnterCriticalSection((LPCRITICAL_SECTION)g_critical);
    	SETUP_THIS_ORIG(int, CIOCPort, GetNewSid, ());
    	int sid = CALL_THIS_ORIG(());
    	LeaveCriticalSection((LPCRITICAL_SECTION)g_critical);
    	return sid;
    }
    Or:
    Code:
    int CIOCPort::GetNewSid()
    {
    	Guard<Mutex> lock(s_sidLock);
    	SETUP_THIS_ORIG(int, CIOCPort, GetNewSid, ());
    	return CALL_THIS_ORIG(());
    }
    Whatever way you go works.

    I went the way I did because of another issue (before I actually dove into why it happened), and then had to replace CEbenezerDlg::OnRefreshButton() to update the socket count on the UI too:
    Code:
    void CEbenezerDlg::OnRefreshButton()
    {
    	m_lblVersion.SetWindowTextA("%s", KOVER_NAME); // officially it points to the version in memory but we don't really care what the version says
    	m_lblSiege.SetWindowTextA(
    		"t:%d, s:%d, r:%d",
    		m_KnightsSiegeWar.m_bySiegeType,
    		m_KnightsSiegeWar.m_bySiegeState,
    		m_KnightsSiegeWar.m_bySiegeUnk);
    	m_lblMode.SetWindowTextA("%d", m_byTestMode);
    	m_lblMoney.SetWindowTextA("%d, %d", m_sCoinBonus, m_sExpBonus);
    	m_lblEvent.SetWindowTextA("%d", m_sFreeDiscount);
    	m_lblDiscount.SetWindowTextA("%d", m_sServerDiscount);
    	m_lblBattle.SetWindowTextA("t=%d, a=%d, v=%d", m_byBattleOpen, m_byBattleAuto, m_byOldVictory);
    	m_lblCount.SetWindowTextA("%d, %d", m_nLoggedInUsers, CIOCPort::GetFreeSlots());
    }
    I left it as such for my own peace of mind, but this isn't really necessary. What matters more is simply guarding the call.

    At this point, you'd think that's the end of it. However, RefreshSidList() is still problematic.

    RefreshSidList()'s sole purpose is to try and restore session IDs to the pool for all inactive sessions.
    It's a perfect example of a check-then-act race condition.

    As above, GetNewSid() will call RefreshSidList() if the sid list is empty:
    Code:
    int CIOCPort::GetNewSid()
    {
      if (m_SidList.empty())
      {
         RefreshSidList();
         return -1;
      }
    So, the assumption is that the session ID list is empty.
    Mildly cleaned up, RefreshSidList() looks like this:
    Code:
    void CIOCPort::RefreshSidList()
    {
      int count = 0;
      for ( int i = 0; i < m_ClientSockSize; ++i )
      {
        if ( !m_ClientSockArray[i] )
        {
          EnterCriticalSection(&g_critical);
          m_SidList.insert(i);
          LeaveCriticalSection(&g_critical);
          ++count;
        }
      }
    
      char Dst[0x400];
      memset(&Dst, 0, 0x400u);
      sprintf(&Dst, "#### RefreshSidList  - refresh sid count=%d  ####\r\n", count);
      LogFileWrite(&Dst);
    }
    So, again, under the assumption that there are no session IDs right now, it's scanning the active session array to see if any aren't actually set.
    For those that aren't set, it's inserting this session ID to the list (no active session, i.e. should be a session ID in the list for it).

    You'll notice that the insert is guarded... but this actually doesn't fix a whole lot.
    Remember, this method's sole purpose is to restore desynced session IDs (aware of its own buggy behaviour) when there's none left to use.

    What's amusing however is the entire scan (including the empty sid list check) should be guarded, because what can happen is that a player can disconnect mid-scan, causing this scan to see that this free slot is inactive and thus its session ID can be restored. Since it's using std::list<>, this then causes it to insert a duplicate session ID to the list... so the next person that connects and is assigned that duplicate session ID (i.e. after the first is assigned) fails to connect, because it will have a session ID but no corresponding session. As the duplicate session ID happens to never be restored when this fails (luckily), it will self-correct so future sessions don't suffer this problem.

    However, it will still cause connection issues for people who do happen to encounter these duplicate session IDs, that should never have been inserted to begin with, so it's still very much a problem.

    At the end of the day, you can do one of 2 things here. Either disable this entire method (when I reimplemented GetNewSid(), I didn't bother to reimplement any such method) or guard it via GetNewSid().

    On another note, as I was far more specific about where I enforced my guards, I removed mgame's own guards to reduce the risk of any deadlocks. I'm aware of some patches which try to use these critical sections without knowing what they're really doing, so this drastically reduces any problems there.
    If you set out to keep RefreshSidList() but guard GetNewSid(), I'd remove RefreshSidList()'s insert guard. Especially so if you use your own mutex instead of theirs (but if you don't, no big deal - if you're using the same mutex, these calls are recursive. The calls are just pointless in this scenario.).

    So to summarise this (probably unnecessarily) long post:

    - AcceptThread doesn't bother guarding anything.
    - Session IDs can desync and duplicate (causing failed connections for people that hit them) without any malicious intent (just regular connections). They can even crash, however soacs adds checks which attempt to stop it from doing so (not entirely reliable though, since it's very dependent on certain scenarios happening which may not always be true).
    - The only things that need to realistically be guarded are those that modify CIOCPort::m_SidList (CIOCPort::GetNewSid(), CIOCPort::PutOldSid()).
    - CIOCPort::RefreshSidList() is a hilarious mess from start to finish.
    - If you got this far you either cheated or have some serious dedication. If it's the latter, I applaud you.

    I hope this leads people to actually go ahead and fix this issue.

    But yeah, if you're still using the official 1.298 binaries I'd encourage you to move away from them and invest in source development instead. This whole thing just solidifies how broken these really are, and having to patch these things manually (while working around things like SOACS getting in your way, which for the record was also causing the server to fall over due to a bug with item sealing in addition to this whole drama) is just really not worth the trouble IMO.
    Last edited by twostars; 08-29-2018 at 02:54 PM.
    Kallop likes this.

  4. #4
    Senior Member
    Join Date
    Sep 2013
    Posts
    63

    Default

    Lol, their problem wasn't socket system at all. Their problem was corrupted user data which soacs was trying to fix that tampered/altered data when certain user was connecting, while trying to correct values of that user it stuck on loophole of detecting length of serials on certain items, as you know for checking len of int/int64 type you keep dividing them by 10 but somehow due to working on 32bit subsystem and trying to calculate big lengthed 64bit serials like having 30 chars it caused problem. Since this files having 1 worker thread accepting packets other packets in queue wasn't getting worked at all and it stuck there.

    This issue you referring above fixed around 5 years or more on SOACS but since you want to blame 1298 files you can blame default ones. All of the said method above fixed on the top of it SOACS verifies user connection if it's passed login server and succesfully authenticated by checking it on directly from database by hooking up "accept" function on winsock so even before
    AcceptThread tries to call connection before allocating session id for it soacs verifies and directly closes that connection, since Workerthread stuck on handling that issue all users who were legit connecting kept spamming this connections, and why you were thinking old issue was happening all online users were disconnected which lead you to believe Socket array broken but reality was since this connections weren't responding Security checks after certain period which actually their answer wasn't handling from Workerthread SOACS disconnected that users.

    In other words you were basically failed to get idea what was wrong while debugged and didn't even ask GlobalKO owner if they have sources which they did have.

  5. #5
    Senior Member
    Join Date
    Dec 2009
    Posts
    1,805

    Default

    Quote Originally Posted by osmanx View Post
    Lol, their problem wasn't socket system at all. Their problem was corrupted user data which soacs was trying to fix that tampered/altered data when certain user was connecting
    Actually, if you were actually monitoring their server, it experienced multiple problems.

    For starters, the flooding (prior to you enabling the whitelisting behaviour).
    As explained, in addition to this consuming all available sockets, the attempts demonstrated the problem explained in great detail by this post.
    Additionally, yes, the soacs issues which I ignored, as they were your responsibility to deal with.

    This issue you referring above fixed around 5 years or more on SOACS but since you want to blame 1298 files you can blame default ones. All of the said method above fixed on the top of it SOACS verifies user connection if it's passed login server and succesfully authenticated by checking it on directly from database by hooking up "accept" function on winsock so even before
    AcceptThread tries to call connection before allocating session id for it soacs verifies and directly closes that connection
    Read my post and you'll see why you're wrong. Pay special attention to where accept() is handled.

  6. #6
    Senior Member
    Join Date
    Sep 2013
    Posts
    63

    Default

    Quote Originally Posted by twostars View Post
    Actually, if you were actually monitoring their server, it experienced multiple problems.

    For starters, the flooding (prior to you enabling the whitelisting behaviour).
    As explained, in addition to this consuming all available sockets, the attempts demonstrated the problem explained in great detail by this post.
    Additionally, yes, the soacs issues which I ignored, as they were your responsibility to deal with.


    Read my post and you'll see why you're wrong. Pay special attention to where accept() is handled.
    It amazes me when people have no clue still saying you are wrong.
    Code:
    DllExport SOCKET myaccept(SOCKET s, const struct sockaddr *name, int *namelen) {
     ThreadGuard::SafeLock guard(ConnectionAcceptSync);
     char IpAddr[1024];memset(IpAddr,NULL,1024);
     sprintf(IpAddr,"%s",GetSocketIP(s));
     if (strnistr(IpAddr,"127.0.0.1") != nullptr) {
      return s;
     }
     if (FilterDB.start()) {
      SQLApi::SafeLock SQLGuard(FilterDB.SQLConnMutex);
      FilterDB.dumpWorker("myaccept");
      char WhiteListedConnection[1024];memset(WhiteListedConnection,NULL,1024);
      sprintf(WhiteListedConnection,"select COUNT(*) from HardwareLog WITH (NOLOCK) where IP = \'%s\' and DATEDIFF(second,LoginTime,GETDATE()) <= %i",IpAddr,AllowedSeconds);
      if (FilterDB.query(WhiteListedConnection)) {
       auto NumConnections = 0;
       if (FilterDB.fetch()) {
        FilterDB.getInt(&NumConnections);
        if (NumConnections == 0) {
         char finalstr[1024]; memset(finalstr, NULL, 1024);
         sprintf(finalstr, "Connection blocked for nonwhitelist!!! %s", IpAddr);
         MainLog.Output("BlockedConnections", finalstr);
         ConsoleOutput(finalstr, LightGrey);
         return(INVALID_SOCKET);
        }
       }
      }
      char finalstr[1024]; memset(finalstr, NULL, 1024);
      sprintf(finalstr, "Connection accepted from %s", IpAddr);
      ConsoleOutput(finalstr, LightRed);
      MainLog.Output("AcceptedConnections", finalstr);
      return(s);
     }
     return(INVALID_SOCKET);
    }
    This is where soacs handles accept and denies connection by checking if user is legit connection.


    All of your above examples in original files caused by wrongly managed CriticalSection handling also fixed, when you were checking via OllyDBG you could easily see them patched like this and other sections of ebenezer.



    Culprit wasn't socket system like I said, it was user named "ZY" had corrupted item data like having serials with larger than usual e.g 978657786456515651 instead of like 76554612345678901 which is standardly having 17 digits.

    Which caused problem on checking if user altered items and fixing them

    Code:
    int numLen64 (__int64 x) { 
     int len = 1;
     __int64 i = 0;
     if (x < 0) { x = x * (-1); }
     for ( i = 10; i<= x; i=i*10) {
      len++;
     }
    }

    Normally this code has no problem but due to working on 32bit subsystem and having larger than usual it ended up being in loop for eternity on that user.

    As I said you were thinking old SOCKET array broken issue was happening because seeing 0 onlines but in reality SecurityVerifier system waited response from each online user since WorkerThread wasn't passing their packets to parsing SecurityVerifier thread labelled them as dodging and disconnected.

    TL;DR: Socket system wasn't issue at all.

  7. #7
    Senior Member
    Join Date
    Dec 2009
    Posts
    1,805

    Default

    succesfully authenticated by checking it on directly from database by hooking up "accept" function on winsock so even before
    AcceptThread tries to call connection before allocating session id for it soacs verifies and directly closes that connection
    Maybe pretty pictures are easier to digest.


    As for the rest of it, great. Some of it may be patched. Doesn't explain why I observed sessions still desyncing though without any deadlock. But at this point it's neither here nor there -- the problem is definitely fixed at this point.
    The solution is here for others.

    That's all that matters.

  8. #8
    Senior Member SteVeO's Avatar
    Join Date
    Mar 2016
    Location
    Frenchies
    Posts
    75

    Default

    Fucking ZY

  9. #9
    Senior Member
    Join Date
    Sep 2013
    Posts
    63

    Default

    Quote Originally Posted by twostars View Post
    Maybe pretty pictures are easier to digest.


    As for the rest of it, great. Some of it may be patched. Doesn't explain why I observed sessions still desyncing though without any deadlock. But at this point it's neither here nor there -- the problem is definitely fixed at this point.
    The solution is here for others.

    That's all that matters.
    Like I said it handles before and went returns INVALID_SOCKET it goes over here

    Code:
    0044435D  |> \53            |PUSH EBX
    0044435E  |.  8BCE          |MOV ECX,ESI
    00444360  |.  FF75 08       |PUSH DWORD PTR SS:[EBP+0x8]
    00444363  |.  E8 6AD7FBFF   |CALL GameServ.00401AD2 // RidIOCPSocket
    00444368  |.  FF75 08       |PUSH DWORD PTR SS:[EBP+0x8]
    0044436B  |.  8BCE          |MOV ECX,ESI
    0044436D  |.  E8 BCE4FBFF   |CALL GameServ.0040282E // PutOldSid
    00444372  |.^ E9 97FEFFFF   |JMP GameServ.0044420E
    00444377  |>  8BCB          |MOV ECX,EBX
    00444379  |.  E8 25E3FBFF   |CALL GameServ.004026A3 //Receive
    0044437E  |.^ E9 8BFEFFFF   \JMP GameServ.0044420E
    What I mean by before not even needed to check if user send VERSION packet to close its connection like you were doing with EbeXtender, trust me I did it as well but It wasn't good enough to do in first place to let their connection keep alive even for certain period.

    What shocked me is you know how long I'm on this shit and selling files did you seriously think I wouldn't fix SID issue for this long? While I fixed it for more than 5 years and when I specially work with almost all 1298 servers. If you want to point issue on 1298 do not refer SOACS in your post first place too. Sadly you failed to realize what was issue, didn't ask them to what they changed lately and do they have access to sources etc, and talking for my mannerism I actually got sick cuz not going sleep to finish work he wanted for 2 days and got fever from my son on top of it so my body as a normal human being needed rest.

  10. #10
    Senior Member
    Join Date
    Dec 2009
    Posts
    1,805

    Default

    Quote Originally Posted by osmanx View Post
    What I mean by before not even needed to check if user send VERSION packet to close its connection like you were doing with EbeXtender, trust me I did it as well but It wasn't good enough to do in first place to let their connection keep alive even for certain period.
    That's very different to what you said then, because before you specifically said that you handled it before AcceptThread which is nonsense. As for the timeouts (and packet validation), they were complementary. Mostly it was just aiming to keep some sessions available so players could get connections in, until you could get it sorted for them properly. I ended up focusing on the socket desync however as I observed sessions were not always being restored to the pool when all connections were disconnected.

    Quote Originally Posted by osmanx View Post
    What shocked me is you know how long I'm on this shit and selling files did you seriously think I wouldn't fix SID issue for this long? While I fixed it for more than 5 years and when I specially work with almost all 1298 servers. If you want to point issue on 1298 do not refer SOACS in your post first place too.
    I apologise if I was overly aggressive, but I'm just stating what I observed. Perhaps your patches missed something, or perhaps they weren't compiled in? I'm sure you can figure out what happened there.

    Edit: Had a look on their test server, and there's no such patches applied when debugging it live. I suspect you may have them disabled without realising it.
    Additionally it'd pay to double-check you addressed everything concerned in this thread when you do, because the RefreshSidList() issue in particular is a subtle one, and well, if you're using your own mutexes make absolute sure you're being consistent about it with the other thread. From the look of your paste you're guarding the caller, but as my post points out that the session ID list is the only thing that really needs to be guarded so you can just move that to PutOldSid() + GetNewSid() which makes it super easy to catch everything. I *think* you're patching GetNewSid() already? If so, just be sure it encompasses the entire method so the empty slot check is guarded as well thereby protecting RefreshSidList(), and then you can go about removing its guards on insert.

    Please don't take offense to this. If everything's fine already, great. Just making sure!

    Quote Originally Posted by osmanx View Post
    Sadly you failed to realize what was issue, didn't ask them to what they changed lately and do they have access to sources etc, and talking for my mannerism I actually got sick cuz not going sleep to finish work he wanted for 2 days and got fever from my son on top of it so my body as a normal human being needed rest.
    I'm sorry you got sick and hope you feel better soon, but there's no need to take it out on me. I merely addressed issues I observed while trying not to step on any toes, even leaving issues with SOACS to you. Apparently I still managed to do so, and even my presence caused them to lose your support, so.. I don't really know what to do about that. It was a one-time thing with them so I hope you forgive them for that.

    Edit:
    Also, I'd just like to clarify one other thing. The server spins up multiple ReceiveWorkerThreads, which you can see at CIOCPort::CreateReceiveWorkerThread().
    So from what I saw, the reason the others died is because they deadlocked because (as you've explained) the SOACS logic bugging out in the other ReceiveWorkerThread had the mutex acquired back up the chain, meaning other worker threads couldn't do anything while this was acquired causing them to just sit there in their deadlocked state.
    Obviously I didn't pay close attention to why it bugged out other than the threads that were deadlocked were 'fine'. I disregarded any threads that directly ref'd SOACS, because I just wanted to be sure the deadlock wasn't my/the server's doing. I didn't want to step on any toes by touching SOACS.

    Hope that clarifies that.
    Last edited by twostars; 08-29-2018 at 04:11 PM.
    UltimatePvP likes this.

  11. #11
    SEXIEST NOSE ON EARTH Senior Member SheldonCooper's Avatar
    Join Date
    Jan 2010
    Posts
    2,794

    Default

    Damn looks like a clash of the titans here.

    But yeah @two, firstly, if it were a soacs issue, over 100 servers on soacs would have been exploited by now, if they didn't use up to date soacs that is (90% of private servers run soacs). Secondly, your code looks like basic mgame source or i'm wrong? And lastly ... So you spent 18 hours fixing an issue that SOACS, according to osman already resolved and failed to see the real reason for their crash was a fix in gamechecks.cpp?

    Do you think people would be buying services from osman if he had shitty files? You do know that there is no other 12xx version that has such a huge base of custom add ons that nobody pulled of yet to integrate on THAT version. YourKO is maybe close but it seems they put a lot of effort into properly adjusting higher source to myko style, since it's done properly, indicators are that it has functions like genie identically working as 18xx integrated in the client the same way, and other features like extending UI windows the way it works on 15xx and higher as well as proper behaviour of skills like blinding, and light shock nova that are significant for 15xx and higher (it's myko based concept but they have been showing off functionality of those 2 skills)

  12. #12
    Senior Member
    Join Date
    Dec 2009
    Posts
    1,805

    Default

    Quote Originally Posted by SheldonCooper View Post
    But yeah @two, firstly, if it were a soacs issue, over 100 servers on soacs would have been exploited by now, if they didn't use up to date soacs that is (90% of private servers run soacs). Secondly, your code looks like basic mgame source or i'm wrong? And lastly ... So you spent 18 hours fixing an issue that SOACS, according to osman already resolved and failed to see the real reason for their crash was a fix in gamechecks.cpp?
    I can only speak for Global-MYKO as that's the only server I've dealt with within the last IDK how many years aside from my own (and likely the last, given how it's clear the 'developers' in this community have not changed a bit), but one: I never claimed it was a SOACS issue (it's not -- it's an issue with mgame's socket logic), and two: contrary to what osmanx stated, there were no such patches in the build they were using. Considering he apparently enabled the whitelist support when he got to looking at their server, I'm assuming that stuff wasn't enabled there either.

    From my perspective, I have nothing to do with SOACS, nor do I really want anything to do with it. I have no idea what may or may not exist in his files -- nor should I be expected to. Even if I had access to the source code, am I actually supposed to be expected to be immediately familiar with everything that exists in it, disabled or not? No.
    The only thing I know for sure is that nothing existed for it in Global-MYKO's server, which I re-confirmed on their test server (which is using the build previous to the one on their live server, that osmanx updated with) after osmanx's post. No such patches exist there, leading me to conclude that like the whitelist patch (which he had to enable), this one is or was probably disabled too. But I can only speculate, since it's not like I have any idea what the deal is with his files.

    So that's what I was working with. As for "spending 18 hours", that's completely wrong. I spent some time looking at it overnight & came back to it sometime later the next day. If that's how you're going to define that then sure. :/

    Finally, as already stated, there were multiple issues at play; the attacks (because the soacs filter wasn't enabled) brought to light this thread's issue. Again, my attention was focused solely on this particular issue, leaving osmanx to deal with fixing SOACS (the bug he mentioned fixing occurring only because he was "checking for corrupted serials" via checking their number of digits -- for what purpose, I have no idea. Not like players ever have access to that stuff, but it's a good thing people never use subservers or this would fail horribly.).

    Edit:
    As for "Secondly, your code looks like basic mgame source or i'm wrong?" -- you'd have to be more specific, but if you're talking about EbeXtender's patch, then yes -- my hooks are fairly neat in that way. I have most of the internal structures mapped for 1.298 & 1.310 so I can (and have) replaced portions of the game code, in addition to -- obviously -- extending it. In this case, I hooked over the method call and implemented my own with what I pasted.

    I do so like this, since it's a debug build with method jumps to make it easier (but detours would also work if that weren't the case):
    Code:
    		static DWORD j_CIOCPort_GetNewSid = 0x00403ECC;
    		static DWORD j_CIOCPort_PutOldSid = 0x0040282E;
    		static DWORD j_CIOCPort_Init = 0x00403107;
    
    		AddMethodHook(j_CIOCPort_GetNewSid, CIOCPort::GetNewSid);
    		AddMethodHook(j_CIOCPort_PutOldSid, CIOCPort::PutOldSid);
    		AddMethodHook(j_CIOCPort_Init, CIOCPort::Init);
    It'll then call my DLL's method (which looks and feels like mgame's) instead of theirs. I've actually replaced a ton of the magic system logic, and various other things (stat calcs) like this.
    My plan was to eventually replace it all, which I'd probably be much further with if not for the server project being a (much better) thing.

    If you mean the rest of it (e.g. the workings of AcceptThread), then yes, that's from mgame's source. I initially had pastes for both to show that they were identical, but apparently there's a 15K character limit per post, so I cut them out. As my post(s) point out, anything I included there is identical unless otherwise stated.

    Quote Originally Posted by SheldonCooper View Post
    Do you think people would be buying services from osman if he had shitty files? You do know that there is no other 12xx version that has such a huge base of custom add ons that nobody pulled of yet to integrate on THAT version. YourKO is maybe close but it seems they put a lot of effort into properly adjusting higher source to myko style, since it's done properly, indicators are that it has functions like genie identically working as 18xx integrated in the client the same way, and other features like extending UI windows the way it works on 15xx and higher as well as proper behaviour of skills like blinding, and light shock nova that are significant for 15xx and higher (it's myko based concept but they have been showing off functionality of those 2 skills)
    What possessed you to even think that any of this is relevant to this thread? Actually, don't answer that because I really don't want to know how much further you'll go to suck osmanx's dick.

  13. #13
    SEXIEST NOSE ON EARTH Senior Member SheldonCooper's Avatar
    Join Date
    Jan 2010
    Posts
    2,794

    Default

    Quote Originally Posted by twostars View Post

    What possessed you to even think that any of this is relevant to this thread? Actually, don't answer that because I really don't want to know how much further you'll go to suck osmanx's dick.
    I don't even have the guy on skype, my brother does deals with osman. I literally haven't spoken with osman since 2016 probably.

    What possessed me? I don't know, maybe I just developed a great amount of social awkwardness, borderline autism even perhaps.

    I dunno maybe it's just all mathematical equation for me that I'm trying to balance out.
    Last edited by SheldonCooper; 08-30-2018 at 07:48 PM.

  14. #14
    GhosT88 Senior Member joecolxvi's Avatar
    Join Date
    Jan 2016
    Posts
    218

    Default

    Just as a honest opinion and not jumping on the wave; TwoStars is always a good source of informations when you are struggling with your KO server he have answered me once and gave me a tiny clue on how to solve a pretty big issue i was facing back then. While OsmanX is also of a huge support when you can get in contact with him (yes he is a really busy guy in a weird way, if you are in a hurry you will simply lose your mind) but credit is given when it's deserved he have helped us more than i can remember and still, as an ex-dev i can say that every dev have it's way of making things and ALL devs make mistakes (thats how they learn) so if you are looking for bugs or logical issues you can find them everywhere (there is no perfect code, there is always room for improvement), anyway i wish both of you best of luck towards making better/more solid/more stable & especially moer secure KO servers that will benefit the whole ko community.

  15. #15
    Senior Member
    Join Date
    Jul 2014
    Posts
    78

    Default

    Quote Originally Posted by twostars View Post
    Maybe pretty pictures are easier to digest.


    As for the rest of it, great. Some of it may be patched. Doesn't explain why I observed sessions still desyncing though without any deadlock. But at this point it's neither here nor there -- the problem is definitely fixed at this point.
    The solution is here for others.

    That's all that matters.
    lol twostarts. You're right, but no one knew it before, or I am wrong?

Similar Threads

  1. Replies: 0
    Last Post: 11-06-2013, 08:41 PM
  2. Leaving this server S> ALL 4 USD
    By Private in forum Ares
    Replies: 34
    Last Post: 08-14-2006, 06:05 PM
  3. this server lag is just rediculous!
    By whassup in forum General Chat
    Replies: 12
    Last Post: 08-10-2006, 02:26 PM
  4. why diez is the only server whos down ?
    By Zorro in forum General Chat
    Replies: 46
    Last Post: 04-19-2006, 10:17 AM

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •