Initial CoreID changes to allow code-based integration

This commit is contained in:
Garrett Mills 2021-10-24 15:37:16 -05:00
parent 1149fc054a
commit 78c57d7747
20 changed files with 261 additions and 98 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ tsconfig.tsbuildinfo
# custom certificates
/ssl-*/
.idea

39
LICENSE Normal file
View File

@ -0,0 +1,39 @@
@coreid/node-radius-server - CoreID maintained fork
Copyright (C) 2021 Simon Tretter, Garrett Mills
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
--
Note: this is a fork of Simon Tretter's node-radius-server package
modified for use in Starship CoreID. Per the terms of GPLv3, the
modifications made are documented below:
- Introduced the PackageInterface helper class
- enables integrating the library into code rather than as a
standalone CLI app
- Modified existing code to use configuration on the PackageInterface
class instead of global imports
- Modified existing code to log through PackageInterface.logger
- Modified existing code to introduce a credentialMiddleware method
to IPacket, which allows external code to customize the credentials
before they are sent
- Modified RadiusService to allow specifying a custom packet decoder
method on PackageInterface.
- Modified UDPServer class to expose a stop() method

View File

@ -1,6 +1,6 @@
{
"name": "radius-server",
"description": "radius server for google LDAP and TTLS",
"name": "@coreid/radius-server",
"description": "CoreID fork of radius server for google LDAP and TTLS",
"version": "1.2.1",
"engines": {
"node": ">13.10.1"
@ -14,7 +14,7 @@
"dist",
"ssl"
],
"homepage": "https://github.com/simllll/node-radius-server",
"homepage": "https://code.garrettmills.dev/starship/node-radius-server",
"scripts": {
"release": "npm run build && standard-version",
"debug": "DEBUG=radius:* node --tls-min-v1.0 dist/app.js",

View File

@ -6,17 +6,21 @@ import * as config from '../config';
import { Authentication } from './auth';
import { IAuthentication } from './types/Authentication';
import { startTLSServer } from './tls/crypt';
import PackageInterface from './interface';
/* test node version */
const testSocket = startTLSServer();
if (typeof testSocket.tls.exportKeyingMaterial !== 'function') {
console.error(`UNSUPPORTED NODE VERSION (${process.version}) FOUND!!`);
const packageInterface = PackageInterface.get();
console.log('min version supported is node js 14. run "sudo npx n 14"');
const prestartServer = () => {
/* test node version */
const testSocket = startTLSServer();
if (typeof testSocket.tls.exportKeyingMaterial !== 'function') {
packageInterface.log(`UNSUPPORTED NODE VERSION (${process.version}) FOUND!!`);
packageInterface.log('min version supported is node js 14. run "sudo npx n 14"');
process.exit(-1);
}
}
const { argv } = yargs
const { argv } = yargs
.usage('NODE RADIUS Server\nUsage: radius-server')
.example('radius-server --port 1812 -s radiussecret', 'start on port 1812 with a secret')
.default({
@ -31,14 +35,15 @@ const { argv } = yargs
.number('port')
.string(['secret', 'authentication']) as {
argv: { port?: number; secret?: string; authentication?: string; authenticationOptions?: any };
};
packageInterface.log(`Listener Port: ${argv.port || 1812}`);
packageInterface.log(`RADIUS Secret: ${argv.secret}`);
packageInterface.log(`Auth ${argv.authentication}`);
packageInterface.log(`Auth Config: ${JSON.stringify(argv.authenticationOptions, undefined, 3)}`);
};
console.log(`Listener Port: ${argv.port || 1812}`);
console.log(`RADIUS Secret: ${argv.secret}`);
console.log(`Auth ${argv.authentication}`);
console.log(`Auth Config: ${JSON.stringify(argv.authenticationOptions, undefined, 3)}`);
(async () => {
const startServer = async () => {
/* configure auth mechansim */
let auth: IAuthentication;
try {
@ -47,7 +52,7 @@ console.log(`Auth Config: ${JSON.stringify(argv.authenticationOptions, undefined
];
auth = new AuthMechanismus(config.authenticationOptions);
} catch (err) {
console.error('cannot load auth mechanismus', config.authentication);
packageInterface.log('cannot load auth mechanismus', config.authentication);
throw err;
}
// start radius server
@ -66,7 +71,7 @@ console.log(`Auth Config: ${JSON.stringify(argv.authenticationOptions, undefined
rinfo.address,
(err, _bytes) => {
if (err) {
console.log('Error sending response to ', rinfo);
packageInterface.log('Error sending response to ', rinfo);
}
},
response.expectAcknowledgment
@ -76,4 +81,9 @@ console.log(`Auth Config: ${JSON.stringify(argv.authenticationOptions, undefined
// start server
await server.start();
})();
};
if (packageInterface.start) {
prestartServer();
startServer();
}

View File

@ -1,8 +1,11 @@
import * as NodeCache from 'node-cache';
import { Cache, ExpirationStrategy, MemoryStorage } from '@hokify/node-ts-cache';
import { IAuthentication } from './types/Authentication';
import PackageInterface from './interface';
const cacheStrategy = new ExpirationStrategy(new MemoryStorage());
const packageInterface = PackageInterface.get();
/**
* this is just a simple abstraction to provide
* an application layer for caching credentials
@ -12,18 +15,25 @@ export class Authentication implements IAuthentication {
constructor(private authenticator: IAuthentication) {}
@Cache(cacheStrategy, { ttl: 60000 })
@Cache(cacheStrategy, { ttl: packageInterface.cacheTTL })
async authenticate(username: string, password: string): Promise<boolean> {
const cacheKey = `usr:${username}|pwd:${password}`;
const fromCache = this.cache.get(cacheKey) as undefined | boolean;
if (fromCache !== undefined) {
console.log(`Cached Auth Result for user ${username}`, fromCache ? 'SUCCESS' : 'Failure');
packageInterface.log(
`Cached Auth Result for user ${username}`,
fromCache ? 'SUCCESS' : 'Failure'
);
return fromCache;
}
const authResult = await this.authenticator.authenticate(username, password);
console.log(`Auth Result for user ${username}`, authResult ? 'SUCCESS' : 'Failure');
this.cache.set(cacheKey, authResult, authResult ? 86400 : 60); // cache for one day on success, otherwise just for 60 seconds
packageInterface.log(`Auth Result for user ${username}`, authResult ? 'SUCCESS' : 'Failure');
this.cache.set(
cacheKey,
authResult,
authResult ? packageInterface.cacheSuccessTTL : packageInterface.cacheFailTTL
); // cache for one day on success, otherwise just for 60 seconds
return authResult;
}

View File

@ -1,12 +1,14 @@
import { ClientOptions, createClient } from 'ldapjs';
import debug from 'debug';
import * as tls from 'tls';
import * as fs from 'fs';
import { IAuthentication } from '../types/Authentication';
import PackageInterface from '../interface';
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
const usernameFields = ['posixUid', 'mail'];
const log = debug('radius:auth:google-ldap');
// TLS:
// https://github.com/ldapjs/node-ldapjs/issues/307
@ -55,7 +57,7 @@ export class GoogleLDAPAuth implements IAuthentication {
};
this.fetchDNs().catch((err) => {
console.error('fatal error google ldap auth, cannot fetch DNs', err);
log('fatal error google ldap auth, cannot fetch DNs', err);
});
}
@ -64,7 +66,7 @@ export class GoogleLDAPAuth implements IAuthentication {
await new Promise<void>((resolve, reject) => {
const ldapDNClient = createClient(this.config).on('error', (error) => {
console.error('Error in ldap', error);
log('Error in ldap', error);
reject(error);
});
@ -92,7 +94,7 @@ export class GoogleLDAPAuth implements IAuthentication {
});
res.on('error', (ldapErr) => {
console.error(`error: ${JSON.stringify(ldapErr)}`);
log(`error: ${JSON.stringify(ldapErr)}`);
reject(ldapErr);
});
@ -144,7 +146,7 @@ export class GoogleLDAPAuth implements IAuthentication {
return this.authenticate(username, password, count, true);
}
// console.log('this.allValidDNsCache', this.allValidDNsCache);
console.error(`invalid username, not found in DN: ${username}`); // , this.allValidDNsCache);
log(`invalid username, not found in DN: ${username}`); // , this.allValidDNsCache);
return false;
}

View File

@ -1,5 +1,9 @@
import axios from 'axios';
import { IAuthentication } from '../types/Authentication';
import PackageInterface from '../interface';
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
interface IHTTPAuthOptions {
url: string;
@ -30,7 +34,7 @@ export class HTTPAuth implements IAuthentication {
return true;
}
console.log(`HTTP authentication failed, response code: ${result.status}`);
log(`HTTP authentication failed, response code: ${result.status}`);
return false;
}

View File

@ -1,5 +1,9 @@
import * as imaps from 'imap-simple';
import { IAuthentication } from '../types/Authentication';
import PackageInterface from '../interface';
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
interface IIMAPAuthOptions {
host: string;
@ -34,7 +38,7 @@ export class IMAPAuth implements IAuthentication {
if (this.validHosts) {
const domain = username.split('@').pop();
if (!domain || !this.validHosts.includes(domain)) {
console.info('invalid or no domain in username', username, domain);
log('invalid or no domain in username', username, domain);
return false;
}
}
@ -57,7 +61,7 @@ export class IMAPAuth implements IAuthentication {
connection.end();
} catch (err) {
console.error('imap auth failed', err);
log('imap auth failed', err);
}
return success;
}

View File

@ -1,6 +1,10 @@
import * as LdapAuth from 'ldapauth-fork';
import * as fs from 'fs';
import { IAuthentication } from '../types/Authentication';
import PackageInterface from '../interface';
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
interface ILDAPAuthOptions {
/** ldap url
@ -44,7 +48,7 @@ export class LDAPAuth implements IAuthentication {
reconnect: true,
});
this.ldap.on('error', (err) => {
console.error('LdapAuth: ', err);
log('LdapAuth: ', err);
});
}
@ -53,7 +57,7 @@ export class LDAPAuth implements IAuthentication {
this.ldap.authenticate(username, password, (err, user) => {
if (err) {
resolve(false);
console.error('ldap error', err);
log('ldap error', err);
// reject(err);
}
if (user) resolve(user);

View File

@ -1,5 +1,8 @@
import { SMTPClient } from 'smtp-client';
import { IAuthentication } from '../types/Authentication';
import PackageInterface from '../interface';
const log = (...args) => PackageInterface.get().log(...args);
interface ISMTPAuthOptions {
host: string;
@ -37,7 +40,7 @@ export class SMTPAuth implements IAuthentication {
if (this.validHosts) {
const domain = username.split('@').pop();
if (!domain || !this.validHosts.includes(domain)) {
console.info('invalid or no domain in username', username, domain);
log('invalid or no domain in username', username, domain);
return false;
}
}
@ -61,7 +64,7 @@ export class SMTPAuth implements IAuthentication {
s.close(); // runs QUIT command
} catch (err) {
console.error('imap auth failed', err);
log('imap auth failed', err);
}
return success;
}

47
src/interface.ts Normal file
View File

@ -0,0 +1,47 @@
import * as radius from 'radius';
import * as config from '../config';
import { IPacket } from './types/PacketHandler';
export type PacketDecoder = (msg: Buffer) => {
packet?: radius.RadiusPacket & IPacket;
secret: string;
};
export default class PackageInterface {
private static _instance?: PackageInterface;
public static get(): PackageInterface {
if (!this._instance) {
this._instance = new PackageInterface();
}
return this._instance;
}
public packetDecoder?: PacketDecoder;
public start = true;
public cacheTTL = 60000;
public cacheSuccessTTL = 86400;
public cacheFailTTL = 60;
public logger: (...any: unknown[]) => unknown = console.log; // eslint-disable-line no-console
private config: any = config;
public log(...any: unknown[]): void {
this.logger(...any);
}
public getConfig(): any {
return this.config;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public setConfig(inConfig: any) {
this.config = inConfig;
}
}

View File

@ -1,8 +1,11 @@
import * as radius from 'radius';
import { IAuthentication } from '../types/Authentication';
import { IPacketHandlerResult, PacketResponseCode } from '../types/PacketHandler';
import {IPacket, IPacketHandlerResult, PacketResponseCode} from '../types/PacketHandler';
import { PacketHandler } from './PacketHandler';
import PackageInterface from '../interface';
const packageInterface = PackageInterface.get();
export class RadiusService {
private packetHandler: PacketHandler;
@ -11,13 +14,29 @@ export class RadiusService {
this.packetHandler = new PacketHandler(authentication);
}
defaultDecoder(msg: Buffer): { packet?: radius.RadiusPacket & IPacket; secret: string } {
const packet = radius.decode({ packet: msg, secret: this.secret });
return {
packet,
secret: this.secret,
};
}
async handleMessage(
msg: Buffer
): Promise<{ data: Buffer; expectAcknowledgment?: boolean } | undefined> {
const packet = radius.decode({ packet: msg, secret: this.secret });
const { packet, secret } = packageInterface.packetDecoder
? packageInterface.packetDecoder(msg)
: this.defaultDecoder(msg);
if (!packet) {
packageInterface.log('Unable to parse packet from message.');
return undefined;
}
if (packet.code !== 'Access-Request') {
console.error('unknown packet type: ', packet.code);
packageInterface.log('unknown packet type: ', packet.code);
return undefined;
}
@ -33,7 +52,7 @@ export class RadiusService {
data: radius.encode_response({
packet,
code: response.code,
secret: this.secret,
secret,
attributes: response.attributes,
}),
// if message is accept or reject, we conside this as final message

View File

@ -1,13 +1,14 @@
// https://tools.ietf.org/html/rfc3748#section-4.1
import * as NodeCache from 'node-cache';
import debug from 'debug';
import { makeid } from '../../helpers';
import { IPacket, IPacketHandler, IPacketHandlerResult } from '../../types/PacketHandler';
import { IEAPMethod } from '../../types/EAPMethod';
import { buildEAPResponse, decodeEAPHeader } from './eap/EAPHelper';
import PackageInterface from '../../interface';
const log = debug('radius:eap');
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
export class EAPPacketHandler implements IPacketHandler {
private identities = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds
@ -66,15 +67,15 @@ export class EAPPacketHandler implements IPacketHandler {
return buildEAPResponse(identifier, 3); // NAK
case 2: // notification
log('>>>>>>>>>>>> REQUEST FROM CLIENT: notification', {});
console.info('notification');
log('notification');
break;
case 4: // md5-challenge
log('>>>>>>>>>>>> REQUEST FROM CLIENT: md5-challenge', {});
console.info('md5-challenge');
log('md5-challenge');
break;
case 254: // expanded type
console.error('not implemented type', type);
log('not implemented type', type);
break;
case 3: // nak
// console.log('got NAK', data);
@ -118,7 +119,7 @@ export class EAPPacketHandler implements IPacketHandler {
method.getEAPType()
);
console.error('unsupported type', type, `requesting: ${serverSupportedMethods}`);
log('unsupported type', type, `requesting: ${serverSupportedMethods}`);
return buildEAPResponse(identifier, 3, Buffer.from(serverSupportedMethods));
}
@ -135,7 +136,7 @@ export class EAPPacketHandler implements IPacketHandler {
// silently ignore;
return {};
} catch (err) {
console.error(
log(
'decoding of (generic) EAP package failed',
msg,
err,

View File

@ -1,4 +1,3 @@
import debug from 'debug';
import { IAuthentication } from '../../types/Authentication';
import {
IPacket,
@ -6,8 +5,10 @@ import {
IPacketHandlerResult,
PacketResponseCode,
} from '../../types/PacketHandler';
import PackageInterface from '../../interface';
const log = debug('radius:user-pwd');
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
export class UserPasswordPacketHandler implements IPacketHandler {
constructor(private authentication: IAuthentication) {}
@ -29,10 +30,11 @@ export class UserPasswordPacketHandler implements IPacketHandler {
log('username', username, username.toString());
log('token', password, password.toString());
const authenticated = await this.authentication.authenticate(
username.toString(),
password.toString()
);
const [strUsername, strPassword] = packet.credentialMiddleware
? packet.credentialMiddleware(username.toString(), password.toString())
: [username.toString(), password.toString()];
const authenticated = await this.authentication.authenticate(strUsername, strPassword);
if (authenticated) {
// success
return {

View File

@ -1,13 +1,14 @@
// https://tools.ietf.org/html/rfc5281 TTLS v0
// https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented)
/* eslint-disable no-bitwise */
import debug from 'debug';
import { IPacketHandlerResult, PacketResponseCode } from '../../../../types/PacketHandler';
import { IEAPMethod } from '../../../../types/EAPMethod';
import { IAuthentication } from '../../../../types/Authentication';
import { buildEAPResponse, decodeEAPHeader } from '../EAPHelper';
import PackageInterface from '../../../../interface';
const log = debug('radius:eap:gtc');
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
export class EAPGTC implements IEAPMethod {
getEAPType(): number {
@ -56,7 +57,7 @@ export class EAPGTC implements IEAPMethod {
attributes: (success && [['User-Name', username]]) || undefined,
};
} catch (err) {
console.error('decoding of EAP-GTC package failed', msg, err);
log('decoding of EAP-GTC package failed', msg, err);
return {
code: PacketResponseCode.AccessReject,
};

View File

@ -2,10 +2,12 @@
// https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented)
/* eslint-disable no-bitwise */
import { RadiusPacket } from 'radius';
import debug from 'debug';
import { IPacketHandlerResult } from '../../../../types/PacketHandler';
import { IEAPMethod } from '../../../../types/EAPMethod';
import { IAuthentication } from '../../../../types/Authentication';
import PackageInterface from '../../../../interface';
const packageInterface = PackageInterface.get();
export class EAPMD5 implements IEAPMethod {
getEAPType(): number {
@ -27,7 +29,7 @@ export class EAPMD5 implements IEAPMethod {
): Promise<IPacketHandlerResult> {
// not implemented
debug('eap md5 not implemented...');
packageInterface.log('eap md5 not implemented...');
return {};
}

View File

@ -6,8 +6,6 @@ import * as NodeCache from 'node-cache';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { attr_id_to_name, attr_name_to_id } from 'radius';
import debug from 'debug';
import { encodeTunnelPW, ITLSServer, startTLSServer } from '../../../../tls/crypt';
import {
IPacket,
@ -20,8 +18,10 @@ import { MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from '../../../../helpe
import { IEAPMethod } from '../../../../types/EAPMethod';
import { IAuthentication } from '../../../../types/Authentication';
import { secret } from '../../../../../config';
import PackageInterface from '../../../../interface';
const log = debug('radius:eap:ttls');
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
function tlsHasExportKeyingMaterial(tlsSocket): tlsSocket is {
exportKeyingMaterial: (length: number, label: string, context?: Buffer) => Buffer;
@ -276,7 +276,7 @@ export class EAPTTLS implements IEAPMethod {
[[17, encodeTunnelPW(keyingMaterial.slice(0, 64), packet.authenticator, secret)]],
]); // MS-MPPE-Recv-Key
} else {
console.error(
log(
'FATAL: no exportKeyingMaterial method available!!!, you need latest NODE JS, see https://github.com/nodejs/node/pull/31814'
);
}
@ -420,7 +420,7 @@ export class EAPTTLS implements IEAPMethod {
// send response
return responseData; // this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData);
} catch (err) {
console.error('decoding of EAP-TTLS package failed', msg, err);
log('decoding of EAP-TTLS package failed', msg, err);
return {
code: PacketResponseCode.AccessReject,
};

View File

@ -4,6 +4,9 @@ import * as events from 'events';
import { EventEmitter } from 'events';
import { newDeferredPromise } from '../helpers';
import { IServer } from '../types/Server';
import PackageInterface from '../interface';
const packageInterface = PackageInterface.get();
export class UDPServer extends events.EventEmitter implements IServer {
static MAX_RETRIES = 3;
@ -53,7 +56,7 @@ export class UDPServer extends events.EventEmitter implements IServer {
const startServer = newDeferredPromise();
this.server.on('listening', () => {
const address = this.server.address();
console.log(`radius server listening ${address.address}:${address.port}`);
packageInterface.log(`radius server listening ${address.address}:${address.port}`);
this.setupListeners();
startServer.resolve();
@ -72,6 +75,12 @@ export class UDPServer extends events.EventEmitter implements IServer {
return startServer.promise;
}
stop(): Promise<void> {
return new Promise<void>((res) => {
this.server.close(() => res());
});
}
private setupListeners() {
this.server.on('message', (message, rinfo) => this.emit('message', message, rinfo));
}

View File

@ -3,33 +3,36 @@ import * as tls from 'tls';
import { createSecureContext } from 'tls';
import * as crypto from 'crypto';
import * as DuplexPair from 'native-duplexpair';
import debug from 'debug';
import * as NodeCache from 'node-cache';
// import * as constants from 'constants';
import * as config from '../../config';
import PackageInterface from '../interface';
const log = debug('radius:tls');
const packageInterface = PackageInterface.get();
const log = (...args) => packageInterface.log(...args);
// https://nodejs.org/api/tls.html
const tlsOptions: tls.SecureContextOptions = {
...config.certificate,
};
log('tlsOptions', tlsOptions);
const secureContext = createSecureContext(tlsOptions);
export function getTLSSecureContext(): tls.SecureContext {
const tlsOptions: tls.SecureContextOptions = {
...packageInterface.getConfig().certificate,
};
return createSecureContext(tlsOptions);
}
export interface ITLSServer {
events: events.EventEmitter;
tls: tls.TLSSocket;
}
const resumeSessions = new NodeCache({ stdTTL: 86400 }); // session reidentification maximum 1 day
const resumeSessions = new NodeCache({ stdTTL: packageInterface.cacheTTL }); // session reidentification maximum 1 day
export function startTLSServer(): ITLSServer {
const duplexpair = new DuplexPair();
const emitter = new events.EventEmitter();
const cleartext = new tls.TLSSocket(duplexpair.socket1, {
secureContext,
secureContext: getTLSSecureContext(),
isServer: true,
// enableTrace: true,
rejectUnauthorized: false,

View File

@ -16,6 +16,7 @@ export interface IPacketAttributes {
export interface IPacket {
attributes: { [key: string]: string | Buffer };
authenticator?: Buffer;
credentialMiddleware?: (username: string, password: string) => [string, string];
}
export interface IPacketHandler {