private static void LoadRadiusReplyAttributes(Configuration configuration, IRadiusDictionary dictionary) { var replyAttributes = new Dictionary <string, List <RadiusReplyAttributeValue> >(); var section = ConfigurationManager.GetSection("RadiusReply") as RadiusReplyAttributesSection; if (section != null) { foreach (var member in section.Members) { var attribute = member as RadiusReplyAttributeElement; var radiusAttribute = dictionary.GetAttribute(attribute.Name); if (radiusAttribute == null) { throw new ConfigurationErrorsException($"Unknown attribute '{attribute.Name}' in RadiusReply configuration element, please see dictionary"); } if (!replyAttributes.ContainsKey(attribute.Name)) { replyAttributes.Add(attribute.Name, new List <RadiusReplyAttributeValue>()); } try { var value = ParseRadiusReplyAttributeValue(radiusAttribute, attribute.Value); replyAttributes[attribute.Name].Add(new RadiusReplyAttributeValue(value, attribute.When)); } catch (Exception ex) { throw new ConfigurationErrorsException($"Error while parsing attribute '{radiusAttribute.Name}' with {radiusAttribute.Type} value '{attribute.Value}' in RadiusReply configuration element: {ex.Message}"); } } } configuration.RadiusReplyAttributes = replyAttributes; }
/// <summary> /// Create a new server on endpoint /// </summary> /// <param name="localEndpoint"></param> /// <param name="dictionary"></param> /// <param name="serverType"></param> public RadiusServer(IUdpClientFactory udpClientFactory, IPEndPoint localEndpoint, IRadiusDictionary dictionary, RadiusServerType serverType) { _udpClientFactory = udpClientFactory; _localEndpoint = localEndpoint; _dictionary = dictionary; _serverType = serverType; }
/// <summary> /// Create a new server on endpoint /// </summary> /// <param name="localEndpoint"></param> /// <param name="dictionary"></param> /// <param name="serverType"></param> public RadiusServer(IUdpClientFactory udpClientFactory, IPEndPoint localEndpoint, IRadiusDictionary dictionary, RadiusServerType serverType, IPacketHandlerRepository packetHandlerRepository) { _udpClientFactory = udpClientFactory; _localEndpoint = localEndpoint; _dictionary = dictionary; _serverType = serverType; _packetHandlerRepository = packetHandlerRepository; }
/// <summary> /// Create a new server on endpoint with packet handler repository /// </summary> public RadiusServer(ServiceConfiguration serviceConfiguration, IRadiusDictionary dictionary, IRadiusPacketParser radiusPacketParser, CacheService cacheService, ILogger logger) { _serviceConfiguration = serviceConfiguration ?? throw new ArgumentNullException(nameof(serviceConfiguration)); _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); _radiusPacketParser = radiusPacketParser ?? throw new ArgumentNullException(nameof(radiusPacketParser)); _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _localEndpoint = serviceConfiguration.ServiceServerEndpoint; _router = new RadiusRouter(serviceConfiguration, radiusPacketParser, logger); }
/// <summary> /// Parses packet bytes and returns an IRadiusPacket /// </summary> /// <param name="packetBytes"></param> /// <param name="dictionary"></param> /// <param name="sharedSecret"></param> public static IRadiusPacket Parse(Byte[] packetBytes, IRadiusDictionary dictionary, Byte[] sharedSecret) { var packetLength = BitConverter.ToUInt16(packetBytes.Skip(2).Take(2).Reverse().ToArray(), 0); if (packetBytes.Length != packetLength) { throw new InvalidOperationException($"Packet length does not match, expected: {packetLength}, actual: {packetBytes.Length}"); } var packet = new RadiusPacket { SharedSecret = sharedSecret, Identifier = packetBytes[1], Code = (PacketCode)packetBytes[0], }; Buffer.BlockCopy(packetBytes, 4, packet.Authenticator, 0, 16); if (packet.Code == PacketCode.AccountingRequest || packet.Code == PacketCode.DisconnectRequest) { if (!packet.Authenticator.SequenceEqual(CalculateRequestAuthenticator(packet.SharedSecret, packetBytes))) { throw new InvalidOperationException($"Invalid request authenticator in packet {packet.Identifier}, check secret?"); } } // The rest are attribute value pairs var position = 20; var messageAuthenticatorPosition = 0; while (position < packetBytes.Length) { var typecode = packetBytes[position]; var length = packetBytes[position + 1]; if (position + length > packetLength) { throw new ArgumentOutOfRangeException("Go home roamserver, youre drunk"); } var contentBytes = new Byte[length - 2]; Buffer.BlockCopy(packetBytes, position + 2, contentBytes, 0, length - 2); try { if (typecode == 26) // VSA { var vsa = new VendorSpecificAttribute(contentBytes); var vendorAttributeDefinition = dictionary.GetVendorAttribute(vsa.VendorId, vsa.VendorCode); if (vendorAttributeDefinition == null) { _log.Info($"Unknown vsa: {vsa.VendorId}:{vsa.VendorCode}"); } else { try { var content = ParseContentBytes(vsa.Value, vendorAttributeDefinition.Type, typecode, packet.Authenticator, packet.SharedSecret); packet.AddAttributeObject(vendorAttributeDefinition.Name, content); } catch (Exception ex) { _log.Error($"Something went wrong with vsa {vendorAttributeDefinition.Name}", ex); } } } else { var attributeDefinition = dictionary.GetAttribute(typecode); if (attributeDefinition.Code == 80) { messageAuthenticatorPosition = position; } try { var content = ParseContentBytes(contentBytes, attributeDefinition.Type, typecode, packet.Authenticator, packet.SharedSecret); packet.AddAttributeObject(attributeDefinition.Name, content); } catch (Exception ex) { _log.Error($"Something went wrong with {attributeDefinition.Name}", ex); _log.Debug($"Attribute bytes: {contentBytes.ToHexString()}"); } } } catch (KeyNotFoundException) { _log.Warn($"Attribute {typecode} not found in dictionary"); } catch (Exception ex) { _log.Error($"Something went wrong parsing attribute {typecode}", ex); } position += length; } if (messageAuthenticatorPosition != 0) { var messageAuthenticator = packet.GetAttribute <Byte[]>("Message-Authenticator"); var temp = new Byte[16]; var tempPacket = new Byte[packetBytes.Length]; packetBytes.CopyTo(tempPacket, 0); Buffer.BlockCopy(temp, 0, tempPacket, messageAuthenticatorPosition + 2, 16); var calculatedMessageAuthenticator = CalculateMessageAuthenticator(tempPacket, sharedSecret); if (!calculatedMessageAuthenticator.SequenceEqual(messageAuthenticator)) { throw new InvalidOperationException($"Invalid Message-Authenticator in packet {packet.Identifier}"); } } return(packet); }
/// <summary> /// Get the raw packet bytes /// </summary> /// <returns></returns> public Byte[] GetBytes(IRadiusDictionary dictionary) { var packetBytes = new List <Byte> { (Byte)Code, Identifier }; packetBytes.AddRange(new Byte[18]); // Placeholder for length and authenticator var messageAuthenticatorPosition = 0; foreach (var attribute in Attributes) { // todo add logic to check attribute object type matches type in dictionary? foreach (var value in attribute.Value) { var contentBytes = GetAttributeValueBytes(value); var headerBytes = new Byte[2]; var attributeType = dictionary.GetAttribute(attribute.Key); switch (attributeType) { case DictionaryVendorAttribute _attributeType: headerBytes = new Byte[8]; headerBytes[0] = 26; // VSA type var vendorId = BitConverter.GetBytes(_attributeType.VendorId); Array.Reverse(vendorId); Buffer.BlockCopy(vendorId, 0, headerBytes, 2, 4); headerBytes[6] = (Byte)_attributeType.VendorCode; headerBytes[7] = (Byte)(2 + contentBytes.Length); // length of the vsa part break; case DictionaryAttribute _attributeType: headerBytes[0] = attributeType.Code; // Encrypt password if this is a User-Password attribute if (_attributeType.Code == 2) { contentBytes = RadiusPassword.Encrypt(SharedSecret, Authenticator, contentBytes); } else if (_attributeType.Code == 80) // Remember the position of the message authenticator, because it has to be added after everything else { messageAuthenticatorPosition = packetBytes.Count; } break; default: throw new InvalidOperationException($"Unknown attribute {attribute.Key}, check spelling or dictionary"); } headerBytes[1] = (Byte)(headerBytes.Length + contentBytes.Length); packetBytes.AddRange(headerBytes); packetBytes.AddRange(contentBytes); } } // Note the order of the bytes... var packetLengthBytes = BitConverter.GetBytes(packetBytes.Count); packetBytes[2] = packetLengthBytes[1]; packetBytes[3] = packetLengthBytes[0]; var packetBytesArray = packetBytes.ToArray(); if (Code == PacketCode.AccountingRequest || Code == PacketCode.DisconnectRequest || Code == PacketCode.CoaRequest) { var authenticator = CalculateRequestAuthenticator(SharedSecret, packetBytesArray); Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); } else { var authenticator = _requestAuthenticator != null?CalculateResponseAuthenticator(SharedSecret, _requestAuthenticator, packetBytesArray) : Authenticator; Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); } if (messageAuthenticatorPosition != 0) { var temp = new Byte[16]; Buffer.BlockCopy(temp, 0, packetBytesArray, messageAuthenticatorPosition + 2, 16); var messageAuthenticatorBytes = CalculateMessageAuthenticator(packetBytesArray, SharedSecret); Buffer.BlockCopy(messageAuthenticatorBytes, 0, packetBytesArray, messageAuthenticatorPosition + 2, 16); } return(packetBytesArray); }
/// <summary> /// Tries to get a packet from the stream. Returns true if successful /// Returns false if no packet could be parsed or stream is empty ie closing /// </summary> /// <param name="stream"></param> /// <param name="packet"></param> /// <param name="dictionary"></param> /// <returns></returns> public static Boolean TryParsePacketFromStream(Stream stream, out IRadiusPacket packet, IRadiusDictionary dictionary, Byte[] sharedSecret) { var packetHeaderBytes = new Byte[4]; var i = stream.Read(packetHeaderBytes, 0, 4); if (i != 0) { try { var packetLength = BitConverter.ToUInt16(packetHeaderBytes.Reverse().ToArray(), 0); var packetContentBytes = new Byte[packetLength - 4]; stream.Read(packetContentBytes, 0, packetContentBytes.Length); packet = Parse(packetHeaderBytes.Concat(packetContentBytes).ToArray(), dictionary, sharedSecret); return(true); } catch (Exception ex) { _log.Warn("Unable to parse packet from stream", ex); } } packet = null; return(false); }
/// <summary> /// Sends a packet /// </summary> /// <param name="responsePacket"></param> /// <param name="remoteEndpoint"></param> /// <param name="dictionary"></param> private void SendResponsePacket(IRadiusPacket responsePacket, IPEndPoint remoteEndpoint, IRadiusDictionary dictionary) { var responseBytes = responsePacket.GetBytes(dictionary); _server.Send(responseBytes, responseBytes.Length, remoteEndpoint); // todo thread safety... although this implementation will be implicitly thread safeish... _log.Info($"{responsePacket.Code} sent to {remoteEndpoint} Id={responsePacket.Identifier}"); }
/// <summary> /// RadiusPacketParser /// </summary> /// <param name="logger"></param> public RadiusPacketParser(ILogger <RadiusPacketParser> logger, IRadiusDictionary radiusDictionary) { _logger = logger; _radiusDictionary = radiusDictionary; }
/// <summary> /// Read and load settings from appSettings configuration section /// </summary> public static Configuration Load(IRadiusDictionary dictionary) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } var appSettings = ConfigurationManager.AppSettings; var serviceServerEndpointSetting = appSettings["adapter-server-endpoint"]; var radiusSharedSecretSetting = appSettings["radius-shared-secret"]; var firstFactorAuthenticationSourceSettings = appSettings["first-factor-authentication-source"]; var apiUrlSetting = appSettings["multifactor-api-url"]; var apiProxySetting = appSettings["multifactor-api-proxy"]; var bypassSecondFactorPeriodSetting = appSettings["bypass-second-factor-period"]; var bypassSecondFactorWhenApiUnreachableSetting = appSettings["bypass-second-factor-when-api-unreachable"]; var nasIdentifierSetting = appSettings["multifactor-nas-identifier"]; var multiFactorSharedSecretSetting = appSettings["multifactor-shared-secret"]; var logLevelSetting = appSettings["logging-level"]; if (string.IsNullOrEmpty(firstFactorAuthenticationSourceSettings)) { throw new Exception("Configuration error: 'first-factor-authentication-source' element not found"); } if (string.IsNullOrEmpty(serviceServerEndpointSetting)) { throw new Exception("Configuration error: 'adapter-server-endpoint' element not found"); } if (string.IsNullOrEmpty(radiusSharedSecretSetting)) { throw new Exception("Configuration error: 'radius-shared-secret' element not found"); } if (string.IsNullOrEmpty(apiUrlSetting)) { throw new Exception("Configuration error: 'multifactor-api-url' element not found"); } if (string.IsNullOrEmpty(nasIdentifierSetting)) { throw new Exception("Configuration error: 'multifactor-nas-identifier' element not found"); } if (string.IsNullOrEmpty(multiFactorSharedSecretSetting)) { throw new Exception("Configuration error: 'multifactor-shared-secret' element not found"); } if (string.IsNullOrEmpty(logLevelSetting)) { throw new Exception("Configuration error: 'logging-level' element not found"); } if (!Enum.TryParse <AuthenticationSource>(firstFactorAuthenticationSourceSettings, out var firstFactorAuthenticationSource)) { throw new Exception("Configuration error: Can't parse 'first-factor-authentication-source' value. Must be one of: ActiveDirectory, Radius, None"); } if (!TryParseIPEndPoint(serviceServerEndpointSetting, out var serviceServerEndpoint)) { throw new Exception("Configuration error: Can't parse 'adapter-server-endpoint' value"); } var configuration = new Configuration { ServiceServerEndpoint = serviceServerEndpoint, RadiusSharedSecret = radiusSharedSecretSetting, FirstFactorAuthenticationSource = firstFactorAuthenticationSource, ApiUrl = apiUrlSetting, ApiProxy = apiProxySetting, NasIdentifier = nasIdentifierSetting, MultiFactorSharedSecret = multiFactorSharedSecretSetting, LogLevel = logLevelSetting }; if (bypassSecondFactorPeriodSetting != null) { if (int.TryParse(bypassSecondFactorPeriodSetting, out var bypassSecondFactorPeriod)) { configuration.BypassSecondFactorPeriod = bypassSecondFactorPeriod; } } if (bypassSecondFactorWhenApiUnreachableSetting != null) { if (bool.TryParse(bypassSecondFactorWhenApiUnreachableSetting, out var bypassSecondFactorWhenApiUnreachable)) { configuration.BypassSecondFactorWhenApiUnreachable = bypassSecondFactorWhenApiUnreachable; } } switch (configuration.FirstFactorAuthenticationSource) { case AuthenticationSource.ActiveDirectory: case AuthenticationSource.Ldap: LoadActiveDirectoryAuthenticationSourceSettings(configuration); break; case AuthenticationSource.Radius: LoadRadiusAuthenticationSourceSettings(configuration); break; } LoadRadiusReplyAttributes(configuration, dictionary); return(configuration); }
public static ClientConfiguration Load(string name, IRadiusDictionary dictionary, AppSettingsSection appSettings, RadiusReplyAttributesSection radiusReplyAttributesSection, bool requiresClientIdentification) { var radiusClientNasIdentifierSetting = appSettings.Settings["radius-client-nas-identifier"]?.Value; var radiusClientIpSetting = appSettings.Settings["radius-client-ip"]?.Value; var radiusSharedSecretSetting = appSettings.Settings["radius-shared-secret"]?.Value; var firstFactorAuthenticationSourceSettings = appSettings.Settings["first-factor-authentication-source"]?.Value; var bypassSecondFactorWhenApiUnreachableSetting = appSettings.Settings["bypass-second-factor-when-api-unreachable"]?.Value; var multiFactorApiKeySetting = appSettings.Settings["multifactor-nas-identifier"]?.Value; var multiFactorApiSecretSetting = appSettings.Settings["multifactor-shared-secret"]?.Value; if (string.IsNullOrEmpty(firstFactorAuthenticationSourceSettings)) { throw new Exception("Configuration error: 'first-factor-authentication-source' element not found"); } if (string.IsNullOrEmpty(radiusSharedSecretSetting)) { throw new Exception("Configuration error: 'radius-shared-secret' element not found"); } if (string.IsNullOrEmpty(multiFactorApiKeySetting)) { throw new Exception("Configuration error: 'multifactor-nas-identifier' element not found"); } if (string.IsNullOrEmpty(multiFactorApiSecretSetting)) { throw new Exception("Configuration error: 'multifactor-shared-secret' element not found"); } if (!Enum.TryParse <AuthenticationSource>(firstFactorAuthenticationSourceSettings, out var firstFactorAuthenticationSource)) { throw new Exception("Configuration error: Can't parse 'first-factor-authentication-source' value. Must be one of: ActiveDirectory, Radius, None"); } var configuration = new ClientConfiguration { Name = name, RadiusSharedSecret = radiusSharedSecretSetting, FirstFactorAuthenticationSource = firstFactorAuthenticationSource, MultifactorApiKey = multiFactorApiKeySetting, MultiFactorApiSecret = multiFactorApiSecretSetting, }; if (requiresClientIdentification) { if (string.IsNullOrEmpty(radiusClientNasIdentifierSetting) && string.IsNullOrEmpty(radiusClientIpSetting)) { throw new Exception("Configuration error: Either 'radius-client-nas-identifier' or 'radius-client-ip' must be configured"); } if (!string.IsNullOrEmpty(radiusClientNasIdentifierSetting)) { configuration.NasIdentifier = radiusClientNasIdentifierSetting; } else { if (!IPAddress.TryParse(radiusClientIpSetting, out var clientIpAddress)) { throw new Exception("Configuration error: Can't parse 'radius-client-ip' value. Must be valid IPv4 address"); } configuration.Ip = clientIpAddress; } } if (bypassSecondFactorWhenApiUnreachableSetting != null) { if (bool.TryParse(bypassSecondFactorWhenApiUnreachableSetting, out var bypassSecondFactorWhenApiUnreachable)) { configuration.BypassSecondFactorWhenApiUnreachable = bypassSecondFactorWhenApiUnreachable; } } switch (configuration.FirstFactorAuthenticationSource) { case AuthenticationSource.ActiveDirectory: case AuthenticationSource.Ldap: LoadActiveDirectoryAuthenticationSourceSettings(configuration, appSettings); break; case AuthenticationSource.Radius: LoadRadiusAuthenticationSourceSettings(configuration, appSettings); break; } LoadRadiusReplyAttributes(configuration, dictionary, radiusReplyAttributesSection); return(configuration); }
/// <summary> /// Read and load settings from appSettings configuration section /// </summary> public static ServiceConfiguration Load(IRadiusDictionary dictionary, ILogger logger) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } var serviceConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); var appSettingsSection = serviceConfig.GetSection("appSettings"); var appSettings = appSettingsSection as AppSettingsSection; var serviceServerEndpointSetting = appSettings.Settings["adapter-server-endpoint"]?.Value; var apiUrlSetting = appSettings.Settings["multifactor-api-url"]?.Value; var apiProxySetting = appSettings.Settings["multifactor-api-proxy"]?.Value; var logLevelSetting = appSettings.Settings["logging-level"]?.Value; if (string.IsNullOrEmpty(serviceServerEndpointSetting)) { throw new Exception("Configuration error: 'adapter-server-endpoint' element not found"); } if (string.IsNullOrEmpty(apiUrlSetting)) { throw new Exception("Configuration error: 'multifactor-api-url' element not found"); } if (string.IsNullOrEmpty(logLevelSetting)) { throw new Exception("Configuration error: 'logging-level' element not found"); } if (!TryParseIPEndPoint(serviceServerEndpointSetting, out var serviceServerEndpoint)) { throw new Exception("Configuration error: Can't parse 'adapter-server-endpoint' value"); } var configuration = new ServiceConfiguration { ServiceServerEndpoint = serviceServerEndpoint, ApiUrl = apiUrlSetting, ApiProxy = apiProxySetting, LogLevel = logLevelSetting }; var clientConfigFilesPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + Path.DirectorySeparatorChar + "clients"; var clientConfigFiles = Directory.Exists(clientConfigFilesPath) ? Directory.GetFiles(clientConfigFilesPath, "*.config") : new string[0]; if (clientConfigFiles.Length == 0) { //check if we have anything var ffas = appSettings.Settings["first-factor-authentication-source"]?.Value; if (ffas == null) { throw new ConfigurationErrorsException("No clients' config files found. Use one of the *.template files in the /clients folder to customize settings. Then save this file as *.config."); } var radiusReplyAttributesSection = ConfigurationManager.GetSection("RadiusReply") as RadiusReplyAttributesSection; var client = Load("General", dictionary, appSettings, radiusReplyAttributesSection, false); client.Ip = IPAddress.Any; configuration.AddClient(client); configuration.SingleClientMode = true; } else { foreach (var clientConfigFile in clientConfigFiles) { logger.Information($"Loading client configuration from {Path.GetFileName(clientConfigFile)}"); var customConfigFileMap = new ExeConfigurationFileMap(); customConfigFileMap.ExeConfigFilename = clientConfigFile; var config = ConfigurationManager.OpenMappedExeConfiguration(customConfigFileMap, ConfigurationUserLevel.None); var clientSettings = (AppSettingsSection)config.GetSection("appSettings"); var radiusReplyAttributesSection = config.GetSection("RadiusReply") as RadiusReplyAttributesSection; var client = Load(Path.GetFileNameWithoutExtension(clientConfigFile), dictionary, clientSettings, radiusReplyAttributesSection, true); configuration.AddClient(client); } } return(configuration); }
/// <summary> /// RadiusPacketParser /// </summary> public RadiusPacketParser(IRadiusDictionary radiusDictionary) { _radiusDictionary = radiusDictionary; }