diff --git a/DnsServerCore/Dhcp/DhcpServer.cs b/DnsServerCore/Dhcp/DhcpServer.cs new file mode 100644 index 00000000..f0a3c170 --- /dev/null +++ b/DnsServerCore/Dhcp/DhcpServer.cs @@ -0,0 +1,735 @@ +/* +Technitium DNS Server +Copyright (C) 2019 Shreyas Zare (shreyas@technitium.com) + +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 . + +*/ + +using DnsServerCore.Dhcp.Options; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace DnsServerCore.Dhcp +{ + //Dynamic Host Configuration Protocol + //https://tools.ietf.org/html/rfc2131 + + //DHCP Options and BOOTP Vendor Extensions + //https://tools.ietf.org/html/rfc2132 + + //Encoding Long Options in the Dynamic Host Configuration Protocol (DHCPv4) + //https://tools.ietf.org/html/rfc3396 + + //Client Fully Qualified Domain Name(FQDN) Option + //https://tools.ietf.org/html/rfc4702 + + public class DhcpServer : IDisposable + { + #region enum + + enum ServiceState + { + Stopped = 0, + Starting = 1, + Running = 2, + Stopping = 3 + } + + #endregion + + #region variables + + readonly List _udpListeners = new List(); + readonly List _listenerThreads = new List(); + + readonly List _scopes = new List(); + + LogManager _log; + + volatile ServiceState _state = ServiceState.Stopped; + + #endregion + + #region constructor + + public DhcpServer() + { } + + public DhcpServer(ICollection scopes) + { + _scopes.AddRange(scopes); + } + + #endregion + + #region IDisposable + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + Stop(); + + if (_log != null) + _log.Dispose(); + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + } + + #endregion + + #region private + + private void ReadUdpRequestAsync(object parameter) + { + Socket udpListener = parameter as Socket; + EndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); + byte[] recvBuffer = new byte[576]; + int bytesRecv; + + try + { + while (true) + { + remoteEP = new IPEndPoint(IPAddress.Any, 0); + + try + { + bytesRecv = udpListener.ReceiveFrom(recvBuffer, ref remoteEP); + } + catch (SocketException ex) + { + switch (ex.SocketErrorCode) + { + case SocketError.ConnectionReset: + case SocketError.HostUnreachable: + case SocketError.MessageSize: + case SocketError.NetworkReset: + bytesRecv = 0; + break; + + default: + throw; + } + } + + if (bytesRecv > 0) + { + switch ((remoteEP as IPEndPoint).Port) + { + case 67: + case 68: + try + { + ThreadPool.QueueUserWorkItem(ProcessUdpRequestAsync, new object[] { udpListener, remoteEP, new DhcpMessage(new MemoryStream(recvBuffer, 0, bytesRecv, false)) }); + } + catch (Exception ex) + { + LogManager log = _log; + if (log != null) + log.Write(remoteEP as IPEndPoint, ex); + } + + break; + } + } + } + } + catch (Exception ex) + { + if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) + return; //server stopping + + LogManager log = _log; + if (log != null) + log.Write(remoteEP as IPEndPoint, ex); + + throw; + } + } + + private void ProcessUdpRequestAsync(object parameter) + { + object[] parameters = parameter as object[]; + + Socket udpListener = parameters[0] as Socket; + EndPoint remoteEP = parameters[1] as EndPoint; + DhcpMessage request = parameters[2] as DhcpMessage; + + try + { + DhcpMessage response = ProcessDhcpMessage(request, remoteEP as IPEndPoint, udpListener.LocalEndPoint as IPEndPoint); + + //send response + if (response != null) + { + byte[] sendBuffer = new byte[512]; + MemoryStream sendBufferStream = new MemoryStream(sendBuffer); + + response.WriteTo(sendBufferStream); + + //send dns datagram + if (!request.RelayAgentIpAddress.Equals(IPAddress.Any)) + { + //received request via relay agent so send unicast response to relay agent on port 67 + udpListener.SendTo(sendBuffer, 0, (int)sendBufferStream.Position, SocketFlags.None, new IPEndPoint(request.RelayAgentIpAddress, 67)); + } + else if (!request.ClientIpAddress.Equals(IPAddress.Any)) + { + //client is already configured and renewing lease so send unicast response on port 68 + udpListener.SendTo(sendBuffer, 0, (int)sendBufferStream.Position, SocketFlags.None, new IPEndPoint(request.ClientIpAddress, 68)); + } + else + { + //send response as broadcast on port 68 + udpListener.SendTo(sendBuffer, 0, (int)sendBufferStream.Position, SocketFlags.None, new IPEndPoint(IPAddress.Broadcast, 68)); + } + } + } + catch (Exception ex) + { + if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) + return; //server stopping + + LogManager log = _log; + if (log != null) + log.Write(remoteEP as IPEndPoint, ex); + } + } + + private DhcpMessage ProcessDhcpMessage(DhcpMessage request, IPEndPoint remoteEP, IPEndPoint interfaceEP) + { + if (request.OpCode != DhcpMessageOpCode.BootRequest) + return null; + + switch (request.DhcpMessageType?.Type) + { + case DhcpMessageType.Discover: + { + Scope scope = FindScope(request, remoteEP.Address, interfaceEP.Address); + if (scope == null) + return null; //no scope available; do nothing + + if (scope.DelayTime > 0) + Thread.Sleep(scope.DelayTime * 1000); //delay sending offer + + Lease offer = scope.GetOffer(request); + if (offer == null) + throw new DhcpServerException("DHCP Server failed to offer address: address unavailable."); + + List options = scope.GetOptions(request, interfaceEP.Address); + if (options == null) + return null; + + return new DhcpMessage(request, offer.Address, interfaceEP.Address, options); + } + + case DhcpMessageType.Request: + { + //request ip address lease or extend existing lease + Scope scope; + Lease leaseOffer; + + if (request.ServerIdentifier == null) + { + if (request.RequestedIpAddress == null) + { + //renewing or rebinding + + if (request.ClientIpAddress.Equals(IPAddress.Any)) + return null; //client must set IP address in ciaddr; do nothing + + scope = FindScope(request, remoteEP.Address, interfaceEP.Address); + if (scope == null) + { + //no scope available; do nothing + return null; + } + + leaseOffer = scope.GetExistingLeaseOrOffer(request); + if (leaseOffer == null) + { + //no existing lease or offer available for client + //send nak + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(interfaceEP.Address), DhcpOption.CreateEndOption() }); + } + + if (!request.ClientIpAddress.Equals(leaseOffer.Address)) + { + //client ip is incorrect + //send nak + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(interfaceEP.Address), DhcpOption.CreateEndOption() }); + } + } + else + { + //init-reboot + scope = FindScope(request, remoteEP.Address, interfaceEP.Address); + if (scope == null) + { + //no scope available; do nothing + return null; + } + + leaseOffer = scope.GetExistingLeaseOrOffer(request); + if (leaseOffer == null) + { + //no existing lease or offer available for client + //send nak + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(interfaceEP.Address), DhcpOption.CreateEndOption() }); + } + + if (!request.RequestedIpAddress.Address.Equals(leaseOffer.Address)) + { + //the client's notion of its IP address is not correct - RFC 2131 + //send nak + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(interfaceEP.Address), DhcpOption.CreateEndOption() }); + } + } + } + else + { + //selecting offer + + if (request.RequestedIpAddress == null) + return null; //client MUST include this option; do nothing + + if (!request.ServerIdentifier.Address.Equals(interfaceEP.Address)) + return null; //offer declined by client; do nothing + + scope = FindScope(request, remoteEP.Address, interfaceEP.Address); + if (scope == null) + { + //no scope available + //send nak + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(interfaceEP.Address), DhcpOption.CreateEndOption() }); + } + + leaseOffer = scope.GetExistingLeaseOrOffer(request); + if (leaseOffer == null) + { + //no existing lease or offer available for client + //send nak + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(interfaceEP.Address), DhcpOption.CreateEndOption() }); + } + + if (!request.RequestedIpAddress.Address.Equals(leaseOffer.Address)) + { + //requested ip is incorrect + //send nak + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(interfaceEP.Address), DhcpOption.CreateEndOption() }); + } + } + + List options = scope.GetOptions(request, interfaceEP.Address); + if (options == null) + return null; + + scope.CommitLease(leaseOffer); + + //log ip lease + LogManager log = _log; + if (log != null) + log.Write(remoteEP as IPEndPoint, "DHCP Server leased IP address [" + leaseOffer.Address.ToString() + "] to " + request.GetClientFullIdentifier() + "."); + + return new DhcpMessage(request, leaseOffer.Address, interfaceEP.Address, options); + } + + case DhcpMessageType.Decline: + { + //ip address is already in use as detected by client via ARP + + if ((request.ServerIdentifier == null) || (request.RequestedIpAddress == null)) + return null; //client MUST include these option; do nothing + + if (!request.ServerIdentifier.Address.Equals(interfaceEP.Address)) + return null; //request not for this server; do nothing + + Scope scope = FindScope(request, remoteEP.Address, interfaceEP.Address); + if (scope == null) + return null; //no scope available; do nothing + + Lease lease = scope.GetExistingLeaseOrOffer(request); + if (lease == null) + return null; //no existing lease or offer available for client; do nothing + + if (!lease.Address.Equals(request.RequestedIpAddress.Address)) + return null; //the client's notion of its IP address is not correct; do nothing + + //remove lease since the IP address is used by someone else + scope.ReleaseLease(lease); + + //log issue + LogManager log = _log; + if (log != null) + log.Write(remoteEP as IPEndPoint, "DHCP Server received DECLINE message: " + request.GetClientFullIdentifier() + " detected that IP address [" + lease.Address + "] is already in use."); + + return null; + } + + case DhcpMessageType.Release: + { + //cancel ip address lease + + if (request.ServerIdentifier == null) + return null; //client MUST include this option; do nothing + + if (!request.ServerIdentifier.Address.Equals(interfaceEP.Address)) + return null; //request not for this server; do nothing + + Scope scope = FindScope(request, remoteEP.Address, interfaceEP.Address); + if (scope == null) + return null; //no scope available; do nothing + + Lease lease = scope.GetExistingLeaseOrOffer(request); + if (lease == null) + return null; //no existing lease or offer available for client; do nothing + + if (!lease.Address.Equals(request.ClientIpAddress)) + return null; //the client's notion of its IP address is not correct; do nothing + + //release lease + scope.ReleaseLease(lease); + + //log ip lease release + LogManager log = _log; + if (log != null) + log.Write(remoteEP as IPEndPoint, "DHCP Server released IP address [" + lease.Address.ToString() + "] that was leased to " + request.GetClientFullIdentifier() + "."); + + //do nothing + return null; + } + + case DhcpMessageType.Inform: + { + //need only local config; already has ip address assigned externally/manually + + Scope scope = FindScope(request, remoteEP.Address, interfaceEP.Address); + if (scope == null) + return null; //no scope available; do nothing + + List options = scope.GetOptions(request, interfaceEP.Address); + if (options == null) + return null; + + return new DhcpMessage(request, IPAddress.Any, interfaceEP.Address, options); + } + + default: + return null; + } + } + + private Scope FindScope(DhcpMessage request, IPAddress remoteAddress, IPAddress interfaceAddress) + { + IPAddress address; + + if (request.RelayAgentIpAddress.Equals(IPAddress.Any)) + { + //no relay agent + if (request.ClientIpAddress.Equals(IPAddress.Any)) + { + address = interfaceAddress; //broadcast request + } + else + { + if (!remoteAddress.Equals(request.ClientIpAddress)) + return null; //client ip must match udp src addr + + address = request.ClientIpAddress; //unicast request + } + } + else + { + //relay agent unicast + + if (!remoteAddress.Equals(request.RelayAgentIpAddress)) + return null; //relay ip must match udp src addr + + address = request.RelayAgentIpAddress; + } + + lock (_scopes) + { + foreach (Scope scope in _scopes) + { + if (scope.InterfaceAddress.Equals(interfaceAddress) && scope.IsAddressInRange(address)) + return scope; + } + } + + return null; + } + + private void BindUdpListener(IPEndPoint dhcpEP) + { + Socket udpListener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + try + { + #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom() + + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + const uint IOC_IN = 0x80000000; + const uint IOC_VENDOR = 0x18000000; + const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + + udpListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); + } + + #endregion + + //bind to interface address + udpListener.EnableBroadcast = true; + udpListener.Bind(dhcpEP); + + lock (_udpListeners) + { + _udpListeners.Add(udpListener); + } + + //start reading dhcp packets + Thread listenerThread = new Thread(ReadUdpRequestAsync); + listenerThread.IsBackground = true; + listenerThread.Start(udpListener); + + lock (_listenerThreads) + { + _listenerThreads.Add(listenerThread); + } + } + catch + { + udpListener.Dispose(); + throw; + } + } + + #endregion + + #region public + + public void Start() + { + if (_disposed) + throw new ObjectDisposedException("DhcpServer"); + + if (_state != ServiceState.Stopped) + throw new InvalidOperationException("DHCP Server is already running."); + + _state = ServiceState.Starting; + + IPEndPoint dhcpEP = new IPEndPoint(IPAddress.Any, 67); + + try + { + BindUdpListener(dhcpEP); + + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server was bound successfully."); + } + catch (Exception ex) + { + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server failed bind.\r\n" + ex.ToString()); + } + + lock (_scopes) + { + foreach (Scope scope in _scopes) + { + if (scope.Enabled) + ActivateScope(scope); + } + } + + _state = ServiceState.Running; + } + + public void Stop() + { + if (_state != ServiceState.Running) + return; + + _state = ServiceState.Stopping; + + lock (_udpListeners) + { + foreach (Socket udpListener in _udpListeners) + udpListener.Dispose(); + } + + _listenerThreads.Clear(); + _udpListeners.Clear(); + + lock (_scopes) + { + foreach (Scope scope in _scopes) + scope.Dispose(); + } + + _state = ServiceState.Stopped; + } + + public Scope[] GetScopes() + { + lock (_scopes) + { + return _scopes.ToArray(); + } + } + + public void AddScope(Scope scope) + { + lock (_scopes) + { + foreach (Scope existingScope in _scopes) + { + if (existingScope.Equals(scope)) + return; + } + + scope.LogManager = _log; + + if (scope.Enabled) + ActivateScope(scope); + + _scopes.Add(scope); + } + } + + public void RemoveScope(Scope scope) + { + lock (_scopes) + { + DeactivateScope(scope); + _scopes.Remove(scope); + } + } + + public void ActivateScope(Scope scope) + { + if (scope.IsActive) + return; + + IPAddress interfaceAddress = scope.InterfaceAddress; + IPEndPoint dhcpEP = new IPEndPoint(interfaceAddress, 67); + + if (interfaceAddress.Equals(IPAddress.Any)) + { + scope.SetActive(true); + + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server successfully activated scope '" + scope.Name + "'"); + } + else + { + try + { + BindUdpListener(dhcpEP); + scope.SetActive(true); + + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server successfully activated scope '" + scope.Name + "'"); + } + catch (Exception ex) + { + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server failed to activate scope '" + scope.Name + "'.\r\n" + ex.ToString()); + } + } + } + + public void DeactivateScope(Scope scope) + { + if (!scope.IsActive) + return; + + IPAddress interfaceAddress = scope.InterfaceAddress; + IPEndPoint dhcpEP = new IPEndPoint(interfaceAddress, 67); + + if (interfaceAddress.Equals(IPAddress.Any)) + { + scope.SetActive(false); + + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server successfully deactivated scope '" + scope.Name + "'"); + } + else + { + lock (_udpListeners) + { + foreach (Socket udpListener in _udpListeners) + { + if (dhcpEP.Equals(udpListener.LocalEndPoint)) + { + try + { + udpListener.Dispose(); + scope.SetActive(false); + + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server successfully deactivated scope '" + scope.Name + "'"); + } + catch (Exception ex) + { + LogManager log = _log; + if (log != null) + log.Write(dhcpEP, "DHCP Server failed to deactivated scope '" + scope.Name + "'.\r\n" + ex.ToString()); + } + + return; + } + } + } + } + } + + #endregion + + #region properties + + public LogManager LogManager + { + get { return _log; } + set { _log = value; } + } + + #endregion + } +} diff --git a/DnsServerCore/Dhcp/DhcpServerException.cs b/DnsServerCore/Dhcp/DhcpServerException.cs new file mode 100644 index 00000000..b6b3979d --- /dev/null +++ b/DnsServerCore/Dhcp/DhcpServerException.cs @@ -0,0 +1,46 @@ +/* +Technitium DNS Server +Copyright (C) 2017 Shreyas Zare (shreyas@technitium.com) + +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 . + +*/ + +using System; + +namespace DnsServerCore.Dhcp +{ + public class DhcpServerException : Exception + { + #region constructors + + public DhcpServerException() + : base() + { } + + public DhcpServerException(string message) + : base(message) + { } + + public DhcpServerException(string message, Exception innerException) + : base(message, innerException) + { } + + protected DhcpServerException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { } + + #endregion + } +} diff --git a/DnsServerCore/Dhcp/Exclusion.cs b/DnsServerCore/Dhcp/Exclusion.cs new file mode 100644 index 00000000..c48f2aee --- /dev/null +++ b/DnsServerCore/Dhcp/Exclusion.cs @@ -0,0 +1,53 @@ +/* +Technitium DNS Server +Copyright (C) 2019 Shreyas Zare (shreyas@technitium.com) + +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 . + +*/ + +using System.Net; + +namespace DnsServerCore.Dhcp +{ + public class Exclusion + { + #region variables + + readonly IPAddress _startingAddress; + readonly IPAddress _endingAddress; + + #endregion + + #region constructor + + public Exclusion(IPAddress startingAddress, IPAddress endingAddress) + { + _startingAddress = startingAddress; + _endingAddress = endingAddress; + } + + #endregion + + #region properties + + public IPAddress StartingAddress + { get { return _startingAddress; } } + + public IPAddress EndingAddress + { get { return _endingAddress; } } + + #endregion + } +} diff --git a/DnsServerCore/Dhcp/Lease.cs b/DnsServerCore/Dhcp/Lease.cs new file mode 100644 index 00000000..433668ba --- /dev/null +++ b/DnsServerCore/Dhcp/Lease.cs @@ -0,0 +1,89 @@ +/* +Technitium DNS Server +Copyright (C) 2019 Shreyas Zare (shreyas@technitium.com) + +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 . + +*/ + +using DnsServerCore.Dhcp.Options; +using System; +using System.Net; + +namespace DnsServerCore.Dhcp +{ + public class Lease + { + #region variables + + readonly ClientIdentifierOption _clientIdentifier; + readonly string _hostName; + readonly byte[] _hardwareAddress; + readonly IPAddress _address; + DateTime _leaseObtained; + DateTime _leaseExpires; + + #endregion + + #region constructor + + internal Lease(ClientIdentifierOption clientIdentifier, string hostName, byte[] hardwareAddress, IPAddress address, uint leaseTime) + { + _clientIdentifier = clientIdentifier; + _hostName = hostName; + _hardwareAddress = hardwareAddress; + _address = address; + + ResetLeaseTime(leaseTime); + } + + internal Lease(byte[] hardwareAddress, IPAddress address, uint leaseTime) + : this(new ClientIdentifierOption(1, hardwareAddress), null, hardwareAddress, address, leaseTime) + { } + + #endregion + + #region public + + public void ResetLeaseTime(uint leaseTime) + { + _leaseObtained = DateTime.UtcNow; + _leaseExpires = DateTime.UtcNow.AddSeconds(leaseTime); + } + + #endregion + + #region properties + + internal ClientIdentifierOption ClientIdentifier + { get { return _clientIdentifier; } } + + public string HostName + { get { return _hostName; } } + + public byte[] HardwareAddress + { get { return _hardwareAddress; } } + + public IPAddress Address + { get { return _address; } } + + public DateTime LeaseObtained + { get { return _leaseObtained; } } + + public DateTime LeaseExpires + { get { return _leaseExpires; } } + + #endregion + } +} diff --git a/DnsServerCore/Dhcp/Scope.cs b/DnsServerCore/Dhcp/Scope.cs new file mode 100644 index 00000000..35b9d454 --- /dev/null +++ b/DnsServerCore/Dhcp/Scope.cs @@ -0,0 +1,843 @@ +/* +Technitium DNS Server +Copyright (C) 2019 Shreyas Zare (shreyas@technitium.com) + +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 . + +*/ + +using DnsServerCore.Dhcp.Options; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading; + +namespace DnsServerCore.Dhcp +{ + public class Scope : IDisposable, IEquatable + { + #region variables + + //required parameters + string _name; + bool _enabled; + IPAddress _startingAddress; + IPAddress _endingAddress; + IPAddress _subnetMask; + + //optional parameters + string _domainName; + IPAddress _routerAddress; + IPAddress[] _dnsServers; + IPAddress[] _winsServers; + IPAddress[] _ntpServers; + ClasslessStaticRouteOption.Route[] _staticRoutes; + uint _leaseTime = 86400; //default 1 day lease + ushort _delayTime; + bool _autoRouter; + bool _autoDnsServer; + bool _reservedAddressOffersOnly; + readonly List _exclusions = new List(); + readonly ConcurrentDictionary _reservedAddresses = new ConcurrentDictionary(); + + //leases + readonly ConcurrentDictionary _leases = new ConcurrentDictionary(); + + //computed parameters + IPAddress _networkAddress; + IPAddress _broadcastAddress; + uint _renewTime; + uint _rebindTime; + + //internal parameters + readonly ConcurrentDictionary _offers = new ConcurrentDictionary(); + IPAddress _lastAddressOffered; + const int OFFER_EXPIRY_SECONDS = 120; //2 mins offer expiry + + bool _isActive; + IPAddress _interfaceAddress; + LogManager _log; + + Timer _maintenanceTimer; + const int MAINTENANCE_TIMER_INTERVAL = 60000; + + #endregion + + #region constructor + + public Scope(string name, IPAddress startingAddress, IPAddress endingAddress, IPAddress subnetMask, bool enabled) + { + _name = name; + _enabled = enabled; + + ChangeNetwork(startingAddress, endingAddress, subnetMask); + + _renewTime = _leaseTime / 2; + _rebindTime = Convert.ToUInt32(_leaseTime * 0.875); + + StartMaintenanceTimer(); + } + + #endregion + + #region IDisposable + + bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + if (_maintenanceTimer != null) + _maintenanceTimer.Dispose(); + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + } + + #endregion + + #region static + + public static bool IsAddressInRange(IPAddress address, IPAddress startingAddress, IPAddress endingAddress) + { + uint addressNumber = ConvertIpToNumber(address); + uint startingAddressNumber = ConvertIpToNumber(startingAddress); + uint endingAddressNumber = ConvertIpToNumber(endingAddress); + + return (startingAddressNumber <= addressNumber) && (addressNumber <= endingAddressNumber); + } + + #endregion + + #region private + + private static uint ConvertIpToNumber(IPAddress address) + { + byte[] addr = address.GetAddressBytes(); + Array.Reverse(addr); + return BitConverter.ToUInt32(addr, 0); + } + + private static IPAddress ConvertNumberToIp(uint address) + { + byte[] addr = BitConverter.GetBytes(address); + Array.Reverse(addr); + return new IPAddress(addr); + } + + private bool IsAddressAvailable(ref IPAddress address) + { + if (address.Equals(_routerAddress)) + return false; + + if ((_dnsServers != null) && _dnsServers.Contains(address)) + return false; + + if ((_winsServers != null) && _winsServers.Contains(address)) + return false; + + if ((_ntpServers != null) && _ntpServers.Contains(address)) + return false; + + lock (_exclusions) + { + foreach (Exclusion exclusion in _exclusions) + { + if (IsAddressInRange(address, exclusion.StartingAddress, exclusion.EndingAddress)) + { + address = exclusion.EndingAddress; + return false; + } + } + } + + foreach (KeyValuePair reservedAddress in _reservedAddresses) + { + if (address.Equals(reservedAddress.Value.Address)) + return false; + } + + foreach (KeyValuePair lease in _leases) + { + if (address.Equals(lease.Value.Address)) + return false; + } + + return true; + } + + private ClientFullyQualifiedDomainNameOption GetClientFullyQualifiedDomainNameOption(DhcpMessage request) + { + ClientFullyQualifiedDomainNameFlags responseFlags = ClientFullyQualifiedDomainNameFlags.None; + + if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat)) + responseFlags |= ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat; + + if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.NoDnsUpdate)) + { + responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns; + responseFlags |= ClientFullyQualifiedDomainNameFlags.OverrideByServer; + } + else if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns)) + { + responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns; + } + else + { + responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns; + responseFlags |= ClientFullyQualifiedDomainNameFlags.OverrideByServer; + } + + string responseDomainName; + + if (request.ClientFullyQualifiedDomainName.DomainName == "") + { + //client domain empty and expects server for a fqdn domain name + if (request.HostName == null) + return null; //server unable to decide a name for client + + responseDomainName = request.HostName.HostName + "." + _domainName; + } + else if (request.ClientFullyQualifiedDomainName.DomainName.Contains(".")) + { + //client domain is fqdn + if (request.ClientFullyQualifiedDomainName.DomainName.EndsWith("." + _domainName, StringComparison.OrdinalIgnoreCase)) + { + responseDomainName = request.ClientFullyQualifiedDomainName.DomainName; + } + else + { + string[] parts = request.ClientFullyQualifiedDomainName.DomainName.Split('.'); + responseDomainName = parts[0] + "." + _domainName; + } + } + else + { + //client domain is just hostname + responseDomainName = request.ClientFullyQualifiedDomainName.DomainName + "." + _domainName; + } + + return new ClientFullyQualifiedDomainNameOption(responseFlags, 255, 255, responseDomainName); + } + + private void StartMaintenanceTimer() + { + if (_maintenanceTimer == null) + { + _maintenanceTimer = new Timer(delegate (object state) + { + try + { + List expiredOffers = new List(); + DateTime utcNow = DateTime.UtcNow; + + foreach (KeyValuePair offer in _offers) + { + if (offer.Value.LeaseObtained.AddSeconds(OFFER_EXPIRY_SECONDS) > utcNow) + { + //offer expired + expiredOffers.Add(offer.Key); + } + } + + foreach (ClientIdentifierOption expiredOffer in expiredOffers) + _offers.TryRemove(expiredOffer, out _); + } + catch (Exception ex) + { + LogManager log = _log; + if (log != null) + log.Write(ex); + } + finally + { + if (!_disposed) + _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); + } + }, null, MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); + } + } + + #endregion + + #region internal + + internal bool IsAddressInRange(IPAddress address) + { + return IsAddressInRange(address, _startingAddress, _endingAddress); + } + + internal Lease GetOffer(DhcpMessage request) + { + if (_leases.TryGetValue(request.ClientIdentifier, out Lease existingLease)) + { + //lease already exists + return existingLease; + } + + if (_reservedAddresses.TryGetValue(request.ClientIdentifier, out Lease existingReservedAddress)) + { + //reserved address exists + Lease reservedOffer = new Lease(request.ClientIdentifier, request.HostName?.HostName, request.ClientHardwareAddress, existingReservedAddress.Address, _leaseTime); + + return _offers.AddOrUpdate(request.ClientIdentifier, reservedOffer, delegate (ClientIdentifierOption key, Lease existingValue) + { + return reservedOffer; + }); + } + + if (_reservedAddressOffersOnly) + return null; //client does not have reserved address as per scope requirements + + Lease dummyOffer = new Lease(request.ClientIdentifier, request.HostName?.HostName, request.ClientHardwareAddress, null, _leaseTime); + Lease existingOffer = _offers.GetOrAdd(request.ClientIdentifier, dummyOffer); + + if (dummyOffer != existingOffer) + { + if (existingOffer.Address == null) + return null; //dummy offer so another thread is handling offer; do nothing + + //offer already exists + existingOffer.ResetLeaseTime(_leaseTime); + + return existingOffer; + } + + //find offer ip address + IPAddress offerAddress = null; + + if (request.RequestedIpAddress != null) + { + //client wish to get this address + IPAddress requestedAddress = request.RequestedIpAddress.Address; + + if (IsAddressInRange(requestedAddress) && IsAddressAvailable(ref requestedAddress)) + offerAddress = requestedAddress; + } + + if (offerAddress == null) + { + //find free address from scope + offerAddress = _lastAddressOffered; + bool offerAddressWasResetFromEnd = false; + + while (true) + { + offerAddress = ConvertNumberToIp(ConvertIpToNumber(offerAddress) + 1u); + + if (offerAddress.Equals(_endingAddress)) + { + if (offerAddressWasResetFromEnd) + return null; //ip pool exhausted + + offerAddress = _startingAddress; + offerAddressWasResetFromEnd = true; + continue; + } + + if (IsAddressAvailable(ref offerAddress)) + break; + } + + _lastAddressOffered = offerAddress; + } + + Lease offerLease = new Lease(request.ClientIdentifier, request.HostName?.HostName, request.ClientHardwareAddress, offerAddress, _leaseTime); + + return _offers.AddOrUpdate(request.ClientIdentifier, offerLease, delegate (ClientIdentifierOption key, Lease existingValue) + { + return offerLease; + }); + } + + internal Lease GetExistingLeaseOrOffer(DhcpMessage request) + { + if (_leases.TryGetValue(request.ClientIdentifier, out Lease existingLease)) + return existingLease; + + if (_offers.TryGetValue(request.ClientIdentifier, out Lease existingOffer)) + return existingOffer; + + return null; + } + + internal List GetOptions(DhcpMessage request, IPAddress interfaceAddress) + { + List options = new List(); + + switch (request.DhcpMessageType.Type) + { + case DhcpMessageType.Discover: + options.Add(new DhcpMessageTypeOption(DhcpMessageType.Offer)); + break; + + case DhcpMessageType.Request: + case DhcpMessageType.Inform: + options.Add(new DhcpMessageTypeOption(DhcpMessageType.Ack)); + break; + + default: + return null; + } + + options.Add(new ServerIdentifierOption(interfaceAddress)); + + switch (request.DhcpMessageType.Type) + { + case DhcpMessageType.Discover: + case DhcpMessageType.Request: + options.Add(new IpAddressLeaseTimeOption(_leaseTime)); + options.Add(new RenewalTimeValueOption(_renewTime)); + options.Add(new RebindingTimeValueOption(_rebindTime)); + break; + } + + if (request.ParameterRequestList == null) + { + options.Add(new SubnetMaskOption(_subnetMask)); + options.Add(new BroadcastAddressOption(_broadcastAddress)); + + if (!string.IsNullOrEmpty(_domainName)) + { + options.Add(new DomainNameOption(_domainName)); + + if (request.ClientFullyQualifiedDomainName != null) + options.Add(GetClientFullyQualifiedDomainNameOption(request)); + } + + if (_autoRouter) + options.Add(new RouterOption(new IPAddress[] { interfaceAddress })); + else if (_routerAddress != null) + options.Add(new RouterOption(new IPAddress[] { _routerAddress })); + + if (_autoDnsServer) + options.Add(new DomainNameServerOption(new IPAddress[] { interfaceAddress })); + else if (_dnsServers != null) + options.Add(new DomainNameServerOption(_dnsServers)); + + if (_winsServers != null) + options.Add(new NetBiosNameServerOption(_winsServers)); + + if (_ntpServers != null) + options.Add(new NetworkTimeProtocolServersOption(_ntpServers)); + + if (_staticRoutes != null) + options.Add(new ClasslessStaticRouteOption(_staticRoutes)); + } + else + { + foreach (DhcpOptionCode optionCode in request.ParameterRequestList.OptionCodes) + { + switch (optionCode) + { + case DhcpOptionCode.SubnetMask: + options.Add(new SubnetMaskOption(_subnetMask)); + options.Add(new BroadcastAddressOption(_broadcastAddress)); + break; + + case DhcpOptionCode.DomainName: + if (!string.IsNullOrEmpty(_domainName)) + { + options.Add(new DomainNameOption(_domainName)); + + if (request.ClientFullyQualifiedDomainName != null) + options.Add(GetClientFullyQualifiedDomainNameOption(request)); + } + + break; + + case DhcpOptionCode.Router: + if (_autoRouter) + options.Add(new RouterOption(new IPAddress[] { interfaceAddress })); + else if (_routerAddress != null) + options.Add(new RouterOption(new IPAddress[] { _routerAddress })); + + break; + + case DhcpOptionCode.DomainNameServer: + if (_autoDnsServer) + options.Add(new DomainNameServerOption(new IPAddress[] { interfaceAddress })); + else if (_dnsServers != null) + options.Add(new DomainNameServerOption(_dnsServers)); + + break; + + case DhcpOptionCode.NetBiosOverTcpIpNameServer: + if (_winsServers != null) + options.Add(new NetBiosNameServerOption(_winsServers)); + + break; + + case DhcpOptionCode.NetworkTimeProtocolServers: + if (_ntpServers != null) + options.Add(new NetworkTimeProtocolServersOption(_ntpServers)); + + break; + + case DhcpOptionCode.ClasslessStaticRoute: + if (_staticRoutes != null) + options.Add(new ClasslessStaticRouteOption(_staticRoutes)); + + break; + } + } + } + + options.Add(DhcpOption.CreateEndOption()); + + return options; + } + + internal void CommitLease(Lease lease) + { + lease.ResetLeaseTime(_leaseTime); + + _leases.AddOrUpdate(lease.ClientIdentifier, lease, delegate (ClientIdentifierOption key, Lease existingValue) + { + return lease; + }); + } + + internal void ReleaseLease(Lease lease) + { + _leases.TryRemove(lease.ClientIdentifier, out _); + } + + internal void SetActive(bool isActive) + { + _isActive = isActive; + + if (!_isActive) + _interfaceAddress = null; //remove interface address on deactivation to allow finding it back on activation + } + + #endregion + + #region public + + public void ChangeNetwork(IPAddress startingAddress, IPAddress endingAddress, IPAddress subnetMask) + { + if (startingAddress.AddressFamily != AddressFamily.InterNetwork) + throw new ArgumentException("Address family not supported.", "startingAddress"); + + if (endingAddress.AddressFamily != AddressFamily.InterNetwork) + throw new ArgumentException("Address family not supported.", "endingAddress"); + + if (subnetMask.AddressFamily != AddressFamily.InterNetwork) + throw new ArgumentException("Address family not supported.", "subnetMask"); + + uint startingAddressNumber = ConvertIpToNumber(startingAddress); + uint endingAddressNumber = ConvertIpToNumber(endingAddress); + + if (startingAddressNumber >= endingAddressNumber) + throw new ArgumentException("Ending address must be greater than starting address.", "endingAddress"); + + _startingAddress = startingAddress; + _endingAddress = endingAddress; + _subnetMask = subnetMask; + + //compute other parameters + uint subnetMaskNumber = ConvertIpToNumber(_subnetMask); + uint networkAddressNumber = startingAddressNumber & subnetMaskNumber; + + _networkAddress = ConvertNumberToIp(networkAddressNumber); + _broadcastAddress = ConvertNumberToIp(networkAddressNumber | ~subnetMaskNumber); + + _lastAddressOffered = _startingAddress; + } + + public void AddExclusion(IPAddress startingAddress, IPAddress endingAddress) + { + if (!IsAddressInRange(startingAddress)) + throw new ArgumentOutOfRangeException("startingAddress", "Exclusion address must be in scope range."); + + if (!IsAddressInRange(endingAddress)) + throw new ArgumentOutOfRangeException("endingAddress", "Exclusion address must be in scope range."); + + lock (_exclusions) + { + foreach (Exclusion exclusion in _exclusions) + { + if (IsAddressInRange(startingAddress, exclusion.StartingAddress, exclusion.EndingAddress)) + throw new ArgumentException("Exclusion range overlaps existing exclusion."); + + if (IsAddressInRange(endingAddress, exclusion.StartingAddress, exclusion.EndingAddress)) + throw new ArgumentException("Exclusion range overlaps existing exclusion."); + } + + _exclusions.Add(new Exclusion(startingAddress, endingAddress)); + } + } + + public bool RemoveExclusion(IPAddress startingAddress, IPAddress endingAddress) + { + lock (_exclusions) + { + Exclusion exclusionFound = null; + + foreach (Exclusion exclusion in _exclusions) + { + if (exclusion.StartingAddress.Equals(startingAddress) && exclusion.EndingAddress.Equals(endingAddress)) + { + exclusionFound = exclusion; + break; + } + } + + if (exclusionFound == null) + return false; + + return _exclusions.Remove(exclusionFound); + } + } + + public void AddReservedAddress(byte[] hardwareAddress, IPAddress address) + { + if (!IsAddressInRange(address)) + throw new ArgumentOutOfRangeException("address", "Reserved address must be in scope range."); + + Lease reservedLease = new Lease(hardwareAddress, address, _leaseTime); + + _reservedAddresses.AddOrUpdate(new ClientIdentifierOption(1, hardwareAddress), reservedLease, delegate (ClientIdentifierOption key, Lease existingValue) + { + return reservedLease; + }); + } + + public bool RemoveReservedAddress(byte[] hardwareAddress) + { + return _reservedAddresses.TryRemove(new ClientIdentifierOption(1, hardwareAddress), out _); + } + + public override bool Equals(object obj) + { + if (obj is null) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + return Equals(obj as Scope); + } + + public bool Equals(Scope other) + { + if (other is null) + return false; + + if (!_startingAddress.Equals(other._startingAddress)) + return false; + + if (!_endingAddress.Equals(other._endingAddress)) + return false; + + if (!_subnetMask.Equals(other._subnetMask)) + return false; + + return true; + } + + public override int GetHashCode() + { + var hashCode = 206027136; + hashCode = hashCode * -1521134295 + _startingAddress.GetHashCode(); + hashCode = hashCode * -1521134295 + _endingAddress.GetHashCode(); + hashCode = hashCode * -1521134295 + _subnetMask.GetHashCode(); + return hashCode; + } + + public override string ToString() + { + return _name; + } + + #endregion + + #region properties + + public string Name + { + get { return _name; } + set { _name = value; } + } + + public bool Enabled + { + get { return _enabled; } + set { _enabled = value; } + } + + public IPAddress StartingAddress + { get { return _startingAddress; } } + + public IPAddress EndingAddress + { get { return _endingAddress; } } + + public IPAddress SubnetMask + { get { return _subnetMask; } } + + public string DomainName + { + get { return _domainName; } + set { _domainName = value; } + } + + public IPAddress RouterAddress + { + get { return _routerAddress; } + set { _routerAddress = value; } + } + + public IPAddress[] DnsServers + { + get { return _dnsServers; } + set { _dnsServers = value; } + } + + public IPAddress[] WinsServers + { + get { return _winsServers; } + set { _winsServers = value; } + } + + public IPAddress[] NtpServers + { + get { return _ntpServers; } + set { _ntpServers = value; } + } + + public ClasslessStaticRouteOption.Route[] StaticRoutes + { + get { return _staticRoutes; } + set { _staticRoutes = value; } + } + + public uint LeaseTime + { + get { return _leaseTime; } + set + { + _leaseTime = value; + _renewTime = _leaseTime / 2; + _rebindTime = Convert.ToUInt32(_leaseTime * 0.875); + } + } + + public ushort DelayTime + { + get { return _delayTime; } + set { _delayTime = value; } + } + + public bool AutoRouter + { + get { return _autoRouter; } + set { _autoRouter = value; } + } + + public bool AutoDnsServer + { + get { return _autoDnsServer; } + set { _autoDnsServer = value; } + } + + public bool ReservedAddressOffersOnly + { + get { return _reservedAddressOffersOnly; } + set { _reservedAddressOffersOnly = value; } + } + + public Exclusion[] Exclusions + { + get + { + lock (_exclusions) + { + return _exclusions.ToArray(); + } + } + } + + public ICollection ReservedAddresses + { get { return _reservedAddresses.Values; } } + + public ICollection Leases + { get { return _leases.Values; } } + + public IPAddress NetworkAddress + { get { return _networkAddress; } } + + public IPAddress BroadcastAddress + { get { return _broadcastAddress; } } + + public bool IsActive + { get { return _isActive; } } + + public IPAddress InterfaceAddress + { + get + { + if (_interfaceAddress == null) + { + uint networkAddressNumber = ConvertIpToNumber(_networkAddress); + uint subnetMaskNumber = ConvertIpToNumber(_subnetMask); + + foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) + { + if (nic.OperationalStatus != OperationalStatus.Up) + continue; + + IPInterfaceProperties ipInterface = nic.GetIPProperties(); + + foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses) + { + if (ip.Address.AddressFamily == AddressFamily.InterNetwork) + { + uint addressNumber = ConvertIpToNumber(ip.Address); + + if ((addressNumber & subnetMaskNumber) == networkAddressNumber) + return ip.Address; + } + } + } + + _interfaceAddress = IPAddress.Any; + } + + return _interfaceAddress; + } + } + + internal LogManager LogManager + { + get { return _log; } + set { _log = value; } + } + + #endregion + } +}