Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions APIDOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5898,6 +5898,13 @@ RESPONSE:
"comments": "comments"
}
],
"hostNameOverrides": [
{
"hostName": "my-host",
"hardwareAddress": "00-00-00-00-00-01",
"comments": "comments"
}
],
"allowOnlyReservedLeases": false,
"blockLocallyAdministeredMacAddresses": true,
"ignoreClientIdentifierOption": true
Expand Down Expand Up @@ -5957,6 +5964,7 @@ WHERE:
- `genericOptions` (optional): This feature allows you to define DHCP options that are not yet directly supported. Use a `|` separated list of DHCP option code defined for it and the value in either a colon (:) separated hex string or a normal hex string in format `{option-code}|{hex-string-value}`.
- `exclusions` (optional): A `|` separated list of IP address range in format `{starting address}|{ending address}` that must be excluded or not assigned dynamically to any client by the DHCP server.
- `reservedLeases` (optional): A `|` separated list of reserved IP addresses in format `{host name}|{MAC address}|{reserved IP address}|{comments}` to be assigned to specific clients based on their MAC address.
- `hostNameOverrides` (optional): A `|` separated list of host name overrides in format `{host name}|{MAC address}|{comments}` to override the DNS hostname for specific clients based on their MAC address without reserving a static IP address for them.
- `allowOnlyReservedLeases` (optional): Set this parameter to `true` to stop dynamic IP address allocation and allocate only reserved IP addresses.
- `blockLocallyAdministeredMacAddresses` (optional): Set this parameter to `true` to stop dynamic IP address allocation for clients with locally administered MAC addresses. MAC address with 0x02 bit set in the first octet indicate a locally administered MAC address which usually means that the device is not using its original MAC address.
- `ignoreClientIdentifierOption` (optional): Set this parameter to `true` to always use the client's MAC address as the identifier to allocate lease instead of the Client Identifier (Option 61) provided by the client in the request. Changing this option may cause the existing clients to get a different IP lease on renewal.
Expand Down Expand Up @@ -6024,6 +6032,60 @@ RESPONSE:
}
```

### Add Host Name Override

Adds a host name override entry to the specified scope. Unlike a reserved lease, this only overrides the DNS hostname for the client based on its MAC address; the client still receives a normal dynamically allocated IP address lease.

URL:\
`http://localhost:5380/api/dhcp/scopes/addHostNameOverride?name=Default&hardwareAddress=00:00:00:00:00:00&hostName=my-host`

PERMISSIONS:\
DhcpServer: Modify

HEADERS:
- Authorization: Bearer <token>

WHERE:
- `token`: The session token generated by the `login` or the `createToken` call.
- `name`: The name of the DHCP scope.
- `hardwareAddress`: The MAC address of the client.
- `hostName`: The hostname of the client to override.
- `comments` (optional): Comments for the host name override entry.

RESPONSE:
```
{
"response": {},
"status": "ok"
}
```

### Remove Host Name Override

Removes a host name override entry from the specified scope.

URL:\
`http://localhost:5380/api/dhcp/scopes/removeHostNameOverride?name=Default&hardwareAddress=00:00:00:00:00:00`

PERMISSIONS:\
DhcpServer: Modify

HEADERS:
- Authorization: Bearer <token>

WHERE:
- `token`: The session token generated by the `login` or the `createToken` call.
- `name`: The name of the DHCP scope.
- `hardwareAddress`: The MAC address of the client.

RESPONSE:
```
{
"response": {},
"status": "ok"
}
```

### Enable DHCP Scope

Enables the DHCP scope allowing the server to allocate leases.
Expand Down
25 changes: 17 additions & 8 deletions DnsServerCore/Dhcp/DhcpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,8 @@ private async Task<DhcpMessage> ProcessDhcpMessageAsync(DhcpMessage request, IPE

if (!string.IsNullOrWhiteSpace(scope.DomainName))
{
//get override host name from reserved lease
Lease reservedLease = scope.GetReservedLease(request);
if (reservedLease is not null)
reservedLeaseHostName = reservedLease.HostName;
//get override host name from reserved lease or host name override
reservedLeaseHostName = GetOverrideHostName(scope, request);
}

List<DhcpOption> options = await scope.GetOptionsAsync(request, serverIdentifierAddress, reservedLeaseHostName, _dnsServer);
Expand Down Expand Up @@ -443,10 +441,8 @@ private async Task<DhcpMessage> ProcessDhcpMessageAsync(DhcpMessage request, IPE

if (!string.IsNullOrWhiteSpace(scope.DomainName))
{
//get override host name from reserved lease
Lease reservedLease = scope.GetReservedLease(request);
if (reservedLease is not null)
reservedLeaseHostName = reservedLease.HostName;
//get override host name from reserved lease or host name override
reservedLeaseHostName = GetOverrideHostName(scope, request);
}

List<DhcpOption> options = await scope.GetOptionsAsync(request, serverIdentifierAddress, reservedLeaseHostName, _dnsServer);
Expand Down Expand Up @@ -713,6 +709,19 @@ internal static string GetSanitizedHostName(string hostname)
return sb.ToString();
}

private static string GetOverrideHostName(Scope scope, DhcpMessage request)
{
Lease reservedLease = scope.GetReservedLease(request);
if (reservedLease is not null)
return reservedLease.HostName;

HostNameOverride hostNameOverride = scope.GetHostNameOverride(request);
if (hostNameOverride is not null)
return hostNameOverride.HostName;

return null;
}

internal void AddDnsEntries(Scope scope, Lease lease)
{
if (lease.Type == LeaseType.Reserved)
Expand Down
121 changes: 121 additions & 0 deletions DnsServerCore/Dhcp/HostNameOverride.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
Technitium DNS Server
Copyright (C) 2026 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 <http://www.gnu.org/licenses/>.

*/

using DnsServerCore.Dhcp.Options;
using System.IO;
using TechnitiumLibrary.IO;

namespace DnsServerCore.Dhcp
{
public class HostNameOverride
{
#region variables

readonly ClientIdentifierOption _clientIdentifier;
string _hostName;
readonly byte[] _hardwareAddress;
string _comments;

#endregion

#region constructor

internal HostNameOverride(string hostName, DhcpMessageHardwareAddressType hardwareAddressType, byte[] hardwareAddress, string comments)
{
_clientIdentifier = new ClientIdentifierOption((byte)hardwareAddressType, hardwareAddress);
_hostName = hostName;
_hardwareAddress = hardwareAddress;
_comments = comments;
}

internal HostNameOverride(string hostName, DhcpMessageHardwareAddressType hardwareAddressType, string hardwareAddress, string comments)
: this(hostName, hardwareAddressType, Lease.ParseHardwareAddress(hardwareAddress), comments)
{ }

internal HostNameOverride(BinaryReader bR)
{
byte version = bR.ReadByte();
switch (version)
{
case 1:
_clientIdentifier = DhcpOption.Parse(bR.BaseStream) as ClientIdentifierOption;
_clientIdentifier.ParseOptionValue();

_hostName = bR.BaseStream.ReadShortString();
if (string.IsNullOrWhiteSpace(_hostName))
_hostName = null;

_hardwareAddress = bR.ReadBuffer();

_comments = bR.BaseStream.ReadShortString();
if (string.IsNullOrWhiteSpace(_comments))
_comments = null;

break;

default:
throw new InvalidDataException("HostNameOverride data format version not supported.");
}
}

#endregion

#region public

public void WriteTo(BinaryWriter bW)
{
bW.Write((byte)1); //version

_clientIdentifier.WriteTo(bW.BaseStream);

if (string.IsNullOrWhiteSpace(_hostName))
bW.Write((byte)0);
else
bW.BaseStream.WriteShortString(_hostName);

bW.WriteBuffer(_hardwareAddress);

if (string.IsNullOrWhiteSpace(_comments))
bW.Write((byte)0);
else
bW.BaseStream.WriteShortString(_comments);
}

#endregion

#region properties

internal ClientIdentifierOption ClientIdentifier
{ get { return _clientIdentifier; } }

public string HostName
{ get { return _hostName; } }

public byte[] HardwareAddress
{ get { return _hardwareAddress; } }

public string Comments
{
get { return _comments; }
set { _comments = value; }
}

#endregion
}
}
71 changes: 70 additions & 1 deletion DnsServerCore/Dhcp/Scope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public sealed class Scope : IComparable<Scope>, IDisposable
IReadOnlyCollection<DhcpOption> _genericOptions;
IReadOnlyCollection<Exclusion> _exclusions;
readonly ConcurrentDictionary<ClientIdentifierOption, Lease> _reservedLeases = new ConcurrentDictionary<ClientIdentifierOption, Lease>();
readonly ConcurrentDictionary<ClientIdentifierOption, HostNameOverride> _hostNameOverrides = new ConcurrentDictionary<ClientIdentifierOption, HostNameOverride>();
bool _allowOnlyReservedLeases;
bool _blockLocallyAdministeredMacAddresses;
bool _ignoreClientIdentifierOption;
Expand Down Expand Up @@ -144,6 +145,7 @@ public Scope(Stream s, LogManager log, DhcpServer dhcpServer)
case 8:
case 9:
case 10:
case 11:
_name = s.ReadShortString();
_enabled = bR.ReadBoolean();

Expand Down Expand Up @@ -400,6 +402,19 @@ public Scope(Stream s, LogManager log, DhcpServer dhcpServer)
}
}

if (version >= 11)
{
int count = bR.ReadInt32();
if (count > 0)
{
for (int i = 0; i < count; i++)
{
HostNameOverride hostNameOverride = new HostNameOverride(bR);
_hostNameOverrides.TryAdd(hostNameOverride.ClientIdentifier, hostNameOverride);
}
}
}

break;

default:
Expand Down Expand Up @@ -917,6 +932,16 @@ internal Lease GetReservedLease(DhcpMessage request)
return GetReservedLease(new ClientIdentifierOption((byte)request.HardwareAddressType, request.ClientHardwareAddress), request.GetClientIdentifier(_ignoreClientIdentifierOption));
}

internal HostNameOverride GetHostNameOverride(DhcpMessage request)
{
ClientIdentifierOption hostNameOverridesClientIdentifier = new ClientIdentifierOption((byte)request.HardwareAddressType, request.ClientHardwareAddress);

if (_hostNameOverrides.TryGetValue(hostNameOverridesClientIdentifier, out HostNameOverride hostNameOverride))
return hostNameOverride;

return null;
}

private Lease GetReservedLease(ClientIdentifierOption reservedLeasesClientIdentifier, ClientIdentifierOption clientIdentifier)
{
if (_reservedLeases.TryGetValue(reservedLeasesClientIdentifier, out Lease reservedLease))
Expand Down Expand Up @@ -1502,6 +1527,19 @@ public bool TryRemoveReservedLease(string hardwareAddress)
return false;
}

public bool TryAddHostNameOverride(HostNameOverride hostNameOverride)
{
return _hostNameOverrides.TryAdd(hostNameOverride.ClientIdentifier, hostNameOverride);
}

public bool TryRemoveHostNameOverride(string hardwareAddress)
{
byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress);
ClientIdentifierOption hostNameOverrideClientIdentifier = new ClientIdentifierOption((byte)DhcpMessageHardwareAddressType.Ethernet, hardwareAddressBytes);

return _hostNameOverrides.TryRemove(hostNameOverrideClientIdentifier, out _);
}

public Lease RemoveLease(string hardwareAddress)
{
byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress);
Expand Down Expand Up @@ -1603,7 +1641,7 @@ public void WriteTo(Stream s)
BinaryWriter bW = new BinaryWriter(s);

bW.Write(Encoding.ASCII.GetBytes("SC"));
bW.Write((byte)10); //version
bW.Write((byte)11); //version

s.WriteShortString(_name);
bW.Write(_enabled);
Expand Down Expand Up @@ -1809,6 +1847,13 @@ public void WriteTo(Stream s)
foreach (KeyValuePair<ClientIdentifierOption, Lease> lease in _leases)
lease.Value.WriteTo(bW);
}

{
bW.Write(_hostNameOverrides.Count);

foreach (KeyValuePair<ClientIdentifierOption, HostNameOverride> hostNameOverride in _hostNameOverrides)
hostNameOverride.Value.WriteTo(bW);
}
}

public override bool Equals(object obj)
Expand Down Expand Up @@ -2209,6 +2254,30 @@ public IReadOnlyCollection<Lease> ReservedLeases
}
}

public IReadOnlyCollection<HostNameOverride> HostNameOverrides
{
get
{
List<HostNameOverride> hostNameOverrides = new List<HostNameOverride>(_hostNameOverrides.Count);

foreach (KeyValuePair<ClientIdentifierOption, HostNameOverride> entry in _hostNameOverrides)
hostNameOverrides.Add(entry.Value);

hostNameOverrides.Sort((a, b) => string.Compare(a.HostName, b.HostName, StringComparison.OrdinalIgnoreCase));
return hostNameOverrides;
}
set
{
_hostNameOverrides.Clear();

if ((value is not null) && (value.Count > 0))
{
foreach (HostNameOverride hostNameOverride in value)
_hostNameOverrides.TryAdd(hostNameOverride.ClientIdentifier, hostNameOverride);
}
}
}

public bool AllowOnlyReservedLeases
{
get { return _allowOnlyReservedLeases; }
Expand Down
2 changes: 2 additions & 0 deletions DnsServerCore/DnsWebService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2144,6 +2144,8 @@ private void ConfigureWebServiceRoutes()
_webService.MapGetAndPost("/api/dhcp/scopes/set", _dhcpApi.SetDhcpScopeAsync);
_webService.MapGetAndPost("/api/dhcp/scopes/addReservedLease", _dhcpApi.AddReservedLease);
_webService.MapGetAndPost("/api/dhcp/scopes/removeReservedLease", _dhcpApi.RemoveReservedLease);
_webService.MapGetAndPost("/api/dhcp/scopes/addHostNameOverride", _dhcpApi.AddHostNameOverride);
_webService.MapGetAndPost("/api/dhcp/scopes/removeHostNameOverride", _dhcpApi.RemoveHostNameOverride);
_webService.MapGetAndPost("/api/dhcp/scopes/enable", _dhcpApi.EnableDhcpScopeAsync);
_webService.MapGetAndPost("/api/dhcp/scopes/disable", _dhcpApi.DisableDhcpScope);
_webService.MapGetAndPost("/api/dhcp/scopes/delete", _dhcpApi.DeleteDhcpScope);
Expand Down
Loading