/node-tacacs-plus

Node library for TACACS+ Authentication.

Primary LanguageJavaScriptGNU General Public License v3.0GPL-3.0

tacacs-plus [TACACS+]

NPM

This is a simple TACACS+ library to help with basic encoding and decoding of TACACS+ authentication and authorization packets.

More information on TACACS+ can be found here, https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-05.

Basic Usage

var tacacs = require('tacacs-plus');

// receive or send raw TCP packet (port 49) to a TACACS+ server or client

var decoded = tacacs.decodePacket({ packet: raw_data, secret: 'your_key' });

The decoded object, depending on the sequence of packets, should be something along the lines of this.

{
    "header": {
        "majorVersion": 12,
        "minorVersion": 0,
        "versionByte": 193,
        "type": 1,
        "sequenceNumber": 1,
        "flags": 1,
        "is_encrypted": false,
        "is_singleConnect": false,
        "sessionId": 1,
        "length": 34
    },
    "rawData": {
        "type": "Buffer",
        "data": [ ... ]
    },
    "data": {
        "action": 1,
        "privLvl": 0,
        "authenType": 0,
        "authenService": 0,
        "userLen": 9,
        "portLen": 4,
        "remAddrLen": 13,
        "dataLen": 0,
        "user": "your_user_name",
        "port": "tty10",
        "remAddr": "your_location"
    }
}

In certain instances, the data element may not be populated if there is an issue with the type of messages or the sequence number. You can manually decode a message body using the decode functions in the library.


Creating a Simple Auth Start

If you are creating a client, to create a simple auth start to send to a server, simply do something along the lines of the following code snippit.

var tacacs = require('tacacs-plus');

// create the auth start body
var authStart = tacacs.createAuthStart({
    action: tacacs.TAC_PLUS_AUTHEN_LOGIN,
    privLvl: tacacs.TAC_PLUS_PRIV_LVL_MAX,
    authenAype: tacacs.TAC_PLUS_AUTHEN_TYPE_ASCII,
    authenAervice: tacacs.TAC_PLUS_AUTHEN_SVC_LOGIN,
    user: 'your_user_name',
    port: 'tty10',
    remAddr: 'your_location',
    data: null
});

// create the tacacs+ header
var header = tacacs.createHeader({
    majorVersion: tacacs.TAC_PLUS_MAJOR_VER,
    minorVersion: tacacs.TAC_PLUS_MINOR_VER_DEFAULT,
    type: tacacs.TAC_PLUS_AUTHEN,
    sequenceNumber: 0x1,
    flags: tacacs.TAC_PLUS_UNENCRYPTED_FLAG,
    sessionId: 0x1,
    length: authStart.length
});

// combine the header and body
var authStartPacket = Buffer.concat([header, authStart]);

// open a connection and send the raw packet via TCP to the server (this example is not using encryption)
  • All decode processes take Buffers that are then converted to objects.
  • All create processes take objects and return Buffers of data.

Encryption

You can use the encodeByteData and decodeByteData functions to encrypt and decrypt data packets.

Using encryption requires a shared secret key as well as cryptographically secure random Session ID values.

var crypto = require('crypto');
var tacacs = require('tacacs-plus');

// Generate a random 32-bit session
var sessionIdBytes = crypto.randomBytes(4);
var sessionId = Math.abs(sessionIdBytes.readInt32BE(0));

// create the auth start body
var authStart = tacacs.createAuthStart({
    action: tacacs.TAC_PLUS_AUTHEN_LOGIN,
    privLvl: tacacs.TAC_PLUS_PRIV_LVL_MAX,
    authenType: tacacs.TAC_PLUS_AUTHEN_TYPE_ASCII,
    authenService: tacacs.TAC_PLUS_AUTHEN_SVC_LOGIN,
    user: 'your_user_name',
    port: 'tty10',
    remAddr: 'your_location',
    data: null
});

var version = tacacs.createVersion(tacacs.TAC_PLUS_MAJOR_VER, tacacs.TAC_PLUS_MINOR_VER_DEFAULT);
var sequenceNumber = 1;
var encryptedAuthStart = tacacs.encodeByteData(sessionId, 'your_key', version, sequenceNumber, authStart);

// create the tacacs+ header
var headerOptions = {
    majorVersion: tacacs.TAC_PLUS_MAJOR_VER,
    minorVersion: tacacs.TAC_PLUS_MINOR_VER_DEFAULT,
    type: tacacs.TAC_PLUS_AUTHEN,
    sequenceNumber: sequenceNumber,
    flags: 0x0, // setting this to zero assumes encryption is being used
    sessionId: sessionId,
    length: authStart.length
}
var header = tacacs.createHeader(headerOptions);

var packetToSend = Buffer.concat([header, encryptedAuthStart]);

// open a connection and send the packet via TCP to the server

Sample Communications

Here is a very simple client that sends a auth start packet to a server, then the server responds to the client... this is a very simple "getting started" sample, that requires a lot more development to implement a full workflow, but it illustrates how to start.

For a more complete client example, see examples/client.js.

var crypto = require('crypto');
var tacacs = require('tacacs-plus');

// SAMPLE SERVER

var server = net.createServer(function (c) {
    console.log('Server: Connection opened.');

    c.on('error', function (err) {
        console.log(err);
    });

    c.on('end', function () {
        console.log('Server: Connection closed.');
    });

    c.on('data', function (data) {
        var replyOptions = {};
        var replyHeader = {
            majorVersion: tacacs.TAC_PLUS_MAJOR_VER,
            minorVersion: tacacs.TAC_PLUS_MINOR_VER_DEFAULT,
            type: tacacs.TAC_PLUS_AUTHEN,
            sequenceNumber: 1,
            flags: 0x0,
            sessionId: 0x0,
            length: 0x0
        };

        console.log('Server: Received ', (data ? data.length : 0), ' bytes.');
        console.log('Server: ' + data.toString('hex'));

        var decodedPacket = tacacs.decodePacket({ packet: data, key: 'your_key' });

        console.log('Server: Decoded TACACS+ request.');
        console.log(JSON.stringify(decodedPacket));

        decodedPacket.header = decodedPacket.header || {};

        var replyHeader = decodedPacket.header;
        replyHeader.sequenceNumber++;
        replyHeader.sessionId = decodedPacket.header.sessionId;

        // build the auth reply (this is all the server should ever send for auth)
        // in this example we will send a get password command (TAC_PLUS_AUTHEN_STATUS_GETPASS)
        var replyOptions = {
            status: tacacs.TAC_PLUS_AUTHEN_STATUS_GETPASS,
            flags: 0x0,
            message: 'Please enter your password: ',
            data: null
        };
        var replyBytes = tacacs.createAuthReply(replyOptions);

        replyHeader.length = replyBytes.length;
        var headerBytes = tacacs.createHeader(replyHeader);
        var encryptedResponse = tacacs.encodeByteData(replyHeader.sessionId, 'your_key', tacacs.createVersion(replyHeader.majorVersion, replyHeader.minorVersion), replyHeader.sequenceNumber, replyBytes);

        replyBytes = Buffer.concat([headerBytes, encryptedResponse]);
        c.write(replyBytes);
    });
});

server.on('error', function (err) {
    console.log('Server: ' + err);
});

server.listen({ port: 49 }, function () {
    console.log('Server: listening...');
});


// SIMPLE CLIENT

var client = net.connect(49, '127.0.0.1', function () {
    console.log('Client connected!');

    // now that we've connected, send the first auth packet

    var sessionIdBytes = crypto.randomBytes(4);
    var sessionId = Math.abs(sessionIdBytes.readInt32BE(0));

    // create the auth start body
    var authStart = tacacs.createAuthStart({
        action: tacacs.TAC_PLUS_AUTHEN_LOGIN,
        privLvl: tacacs.TAC_PLUS_PRIV_LVL_MAX,
        authenType: tacacs.TAC_PLUS_AUTHEN_TYPE_ASCII,
        authenService: tacacs.TAC_PLUS_AUTHEN_SVC_LOGIN,
        user: 'your_user_name',
        port: 'tty10',
        remAddr: 'your_location',
        data: null
    });

    var version = tacacs.createVersion(tacacs.TAC_PLUS_MAJOR_VER, tacacs.TAC_PLUS_MINOR_VER_DEFAULT);
    var sequenceNumber = 1;
    var encryptedAuthStart = tacacs.encodeByteData(sessionId, 'your_key', version, sequenceNumber, authStart);

    // create the tacacs+ header
    var headerOptions = {
        majorVersion: tacacs.TAC_PLUS_MAJOR_VER,
        minorVersion: tacacs.TAC_PLUS_MINOR_VER_DEFAULT,
        type: tacacs.TAC_PLUS_AUTHEN,
        sequenceNumber: sequenceNumber,
        flags: 0x0, // setting this to zero assumes encryption is being used
        sessionId: sessionId,
        length: authStart.length
    }
    var header = tacacs.createHeader(headerOptions);

    var packetToSend = Buffer.concat([header, encryptedAuthStart]);

    // send the auth start packet to the server
    console.log('Client: Sending: ' + packetToSend.length + ' bytes.');
    console.log('Client: ' + packetToSend.toString('hex'));
    client.write(packetToSend);
});

client.on('error', function (err) { console.log(err); });
client.on('data', function (data) {
    if (data) {
        console.log('Client: Received Data: ' + data.toString('hex'));
        // decode response
        var resp = tacacs.decodePacket({ packet: data, key: 'your_key' });
        console.log('Client: Decoded Response: ' + JSON.stringify(resp, null, 2));
    }
    else {
        console.log('Client: No data!');
    }
});

Authorization

simple authorization request and responses can also be created by using the createAuthorizationRequest and createAuthorizationResponse and their associated decode processes.

const tacacs = require('tacacs-plus');


var authorReq = tacacs.createAuthorizationRequest({
    authenMethod: tacacs.TAC_PLUS_AUTHEN_METH_NOT_SET,
    privLvl: tacacs.TAC_PLUS_PRIV_LVL_USER,
    authenType: tacacs.TAC_PLUS_AUTHEN_TYPE_ASCII,
    authenService: tacacs.TAC_PLUS_AUTHEN_TYPE_NOT_SET,
    user: 'user',
    port: 'port',
    remAddr: 'rem addr',
    args: ['test=123', 'test1=456']
});

console.log('Author Request: ' + authorReq.toString('hex'));

var decodedReq = tacacs.decodeAuthorizationRequest(authorReq);

console.log('Author Request: ' + JSON.stringify(decodedReq));

console.log('---------');

var authorResp = tacacs.createAuthorizationResponse({
    status: tacacs.TAC_PLUS_AUTHOR_STATUS_ERROR,
    args: ['test=123', 'test1=456'],
    serverMessage: 'Test Message',
    data: 'Test Data'
});

console.log('Author Response: ' + authorResp.toString('hex'));

var decodedResp = tacacs.decodeAuthorizationResponse(authorResp);

console.log('Author Response: ' + JSON.stringify(decodedResp));

Testing Server

A good and easy to spin up testing server is tac_plus running in a docker container. If you have docker setup, simply run the following to start the tac_plus container. More information is available here, https://hub.docker.com/r/dchidell/docker-tacacs.

sudo docker run -it --rm -p 49:49 dchidell/docker-tacacs

Then you can point your client to the docker server IP on port 49 and use the shared key 'ciscotacacskey' and the user 'iosuser' with the password 'cisco'.