Handling errors in login protocol

Started by
6 comments, last by _winterdyne_ 18 years ago
Hey all. I'm currently working on making the connection / disconnection of a service user (client type object) and service (server type object) robust. What's basically happining is a client application implements some kind of 'service user' class (the base of which provides most functionality), and the server app a 'service'. Both communicate and perform login / authentication properly using either TCP (sequential packet) or UDP (non sequential) through a message system. What I'm working on now is what a service user should do in the case of an unexpected failure in communication - for example a UDP client attempting to connect to a service that's not live (and because UDP at the underlying level is connectionless the client thinks it is potentially connected, thus failing receives), or a connection being dropped mid-authentication - not in terms of handling the actual error (not a problem) but in terms of behaviour. TCP obviously provides a more robust connection model (my system supports both) which gives more information, but behaviour under either protocol must be apparently the same (same interface, same defined high-level behaviour). Is it 'nicer' for the service user to automatically reattempt connection and login(without informing the client other than through a status log) or to inform the client that the service is unavailable? Perhaps retry a set number of times? On the service, should I drop the virtual connection (meaning the client must re-authenticate) or maintain a 'linkdead list' of hosts in order to catch a specific 'reconnect' message, or handle a message from a known host and have the server request reauthentication within a certain time? Or not at all? Any thoughts?
Winterdyne Solutions Ltd is recruiting - this thread for details!
Advertisement
I prefer layering that kind of thing. If the connection layer determines that the connection is down, it should report to the layer on top that the connection is down, and let the next layer deal with it.

You could then have a separate session layer which might introduce re-try, linkdead, and other kinds of session semantics. Ideally, the interface between the two is abstract enough that you can switch out transport underneath session (or vice versa) without the other module being the wiser.

Note that you may wish to introduce another state: "lagging," which means that acks are taking a long time in coming, or not coming at all, or the send queue being overly long. Lag spikes or slow modems would cause this state on both UDP and TCP connections. The state is advisory (doesn't change the behavior of the connection), but the session layer could probably put that information to good use.
enum Bool { True, False, FileNotFound };
Yes, it's the service (session management) layer I'm on about. A service or service user can be initialised to use TCP or UDP (this bit works), it's what behaviour to leave at this layer that's the question - I have these layers at the moment:

socket/socket pool ->communication -> service -> application specific

With the service being configurable in a number of ways (presence or not of a login manager, encryption settings). The communication layer detects problems as it despatches messages / attempts to connect and calls functions that are defined at the service / service user level.

What I'm asking is whether to put the remit for reconnection / handling (a certain number of) failed attempts at the service layer or the application layer, and what kind of behaviour would people prefer?
Winterdyne Solutions Ltd is recruiting - this thread for details!
I would prefer that the application requests a connection be maintained, and the server tell the application when there is or isn't a connection.

Thus, when the connection goes down, the service calls back with a notification, but still attempts to re-create the connection. When the connection comes back, it notifies the app again.

The way I would implement it using my favored patterns (request and subscriber) is something like:

class Requester {  public:    virtual void onSuccess() = 0;    virtual void onError( ErrorDetails * details ) = 0;};class Request {  public:    // After start(), requestuser may be called with complete or     // error, after which point the Request is no longer valid.    virtual void start( Requester * user ) = 0;    // After dispose() is called, the user will not be called back     // by the request. This may cancel the underlying operation.    virtual void dispose() = 0;};class Subscriber {  public:    virtual void onDelivery() = 0;};class Subscription {  public:    // Start notifying the user about state changes. Note     // that no callback is made from within start().    virtual void start( Subscriber * user ) = 0;    // Stop notifying the user, and delete the subscription     // instance.    virtual void dispose() = 0;};class Session : public Request {  public:    // The subscription will notify each time the session     // becomes connected or not connected.    virtual Subscription * newSubscription() = 0;    // Check the connected status of the session.    virtual bool connected() = 0;    ... other session functions go here ...};class SessionMaker {  public:    // Create a session. You need to start() it for it     // to actually want to maintain connectedness. When you     // dispose() it, it will disconnect and go away.    // Note that session never calls onSuccess(), but it may     // call onError() if it determines that no connection     // can ever be made.    Session * newSession( SessionData const & data ) = 0;};


The user would do something like:

class User : public Requester, public Subscriber {};void User::startLogin( info ){  mySession_ = sessionMaker_->newSession( info );  mySub_ = mySession_->newSubscription();  mySub_->start( this );  onDelivery();  mySession_->start( this );}void User::onError(){  mySub_->dispose();  mySub_ = 0;  mySession_ = 0;  onDelivery();}void User::onDelivery(){  isConnected_ = mySession_ && mySession_->connected();}void User::poll(){  if( isConnected_ != curConnected_ ) {    .. connected state changed ..    curConnected_ = isConnected_;  }}


This looks verbose, because it includes the subscriber and requester infrastructure, but that should actually be shared across all classes in an application, as they model two patterns that are recurring in software engineering. Once you have those patterns, Session and SessionMaker themselves are really simple!
enum Bool { True, False, FileNotFound };
If you compare to vpn protocols:

In IPSec if you lose the keepalive the connection is terminated.

In PPTP it is the same although I'm not sure that is a must in the standard.

An ssl session may remain even when you close the browser and reopen depending on implementation, though technically you are reauthenticating but the user is unaware of it.

ssh uses keep alives, but again I'm not sure if timing out is a must.


So in conclusion, I guess the higher the security requirements the more likely you'd need to reauthenticate.
I've got something similar to this implemented, and I'm quite happy with how events are handled, but I was more interested in where to implement 'reliability' features (automatic reconnection / resume of authenticated (login and beyond) services).

As far as the interfaces to the network library are concerned, there are only two: a service, and a service user. Both are effectively wrappers around dedicated threads that deal solely with communications activities (including the management and handling of incoming packets / messages).

The procedure for use is basically instantiation (which sets default settings for encryption, packet size, message size, various timeouts), initialisation (which sets non-defaultable settings like host, port, username, password, encryption and hash methods and provides methods to override existing classes or settings such as plugging in a replacement authenticator or specifying a login manager and finally starting the service or connecting the service user.

When started, a service basically just starts listening. Incoming connections are dealt with by two or three aggregated classes in sequence - an authenticator, login manager, and finally a client manager. The authenticator and client manager are mandatory.

The service user when started goes through the authentication process- first sending a client type (which the authenticator can accept or reject), then performing Diffie Hellman with the authenticator, then (if there is a login manager) logging in with the specified username and password hash.

When disconnected, the service user thread at present terminates, but I'm considering having the thread restart a number of times rather than terminate on an unexpected disconnect (such as a network failure). My question was more to do in these situations, as the authentication state of the client changes.

When logged in (fully authenticated) it's desirable that a reconnect not cause a new connection / retrieve from DB (at the application level), but rather be handled as a reconnect - but is it a good idea to handle this by completely re-authenticating, or sending an 'I have a ticket' message? Ticketed authentication (session key) is used when communicating with game servers (as opposed to the master server which handles initial connections to the cluster), so this isn't a major headache.

[Edited by - _winterdyne_ on April 4, 2006 6:02:58 AM]
Winterdyne Solutions Ltd is recruiting - this thread for details!
Quote:I was more interested in where to implement 'reliability' features (automatic reconnection / resume of authenticated (login and beyond) services)


If you analyze the usage of the patterns I'm suggesting, the actual end result is this:

1) The user requests a session, and asks to hear about changes in state on that session.
2) While the session request is outstanding, the user gets told about when the session establishes and disconnects.
3) If the user cancels the session, it stops trying to be connected. Else, it tries to re-establish itself.

In brief, what I'm saying is that, as long as the user wishes for there to be a session, the system should attempt to make sure that there is a session.
enum Bool { True, False, FileNotFound };
Quote:
In brief, what I'm saying is that, as long as the user wishes for there to be a session, the system should attempt to make sure that there is a session.


Cool. That's what I was planning on implementing, just wasn't sure if explicit reactions to state changes should be forced at the user level, rather than having crude behaviour at the service layer. That's got me thinking about a more complex state model to allow the user to interupt or override normal behaviour at the service layer.

Thanks very much!
Winterdyne Solutions Ltd is recruiting - this thread for details!

This topic is closed to new replies.

Advertisement