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
59 changes: 59 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Docker

on:
push:
branches: [master]
tags: ["v*"]

env:
IMAGE: ghcr.io/${{ github.repository_owner }}/dnsserver

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
path: DnsServer

- uses: actions/checkout@v4
with:
repository: TechnitiumSoftware/TechnitiumLibrary
path: TechnitiumLibrary

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0"

- name: Build TechnitiumLibrary
run: |
dotnet build TechnitiumLibrary/TechnitiumLibrary.ByteTree/TechnitiumLibrary.ByteTree.csproj -c Release
dotnet build TechnitiumLibrary/TechnitiumLibrary.Net/TechnitiumLibrary.Net.csproj -c Release
dotnet build TechnitiumLibrary/TechnitiumLibrary.Security.OTP/TechnitiumLibrary.Security.OTP.csproj -c Release

- run: dotnet publish DnsServer/DnsServerApp/DnsServerApp.csproj -c Release

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

- uses: docker/build-push-action@v6
with:
context: DnsServer
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
3 changes: 3 additions & 0 deletions DnsServerApp/DnsServerApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
<None Update="systemd.service">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="systemd.socket">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions DnsServerApp/systemd.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Unit]
Description=Technitium DNS Server Socket

[Socket]
ListenDatagram=53
ListenStream=53
ReceiveBuffer=524288
SendBuffer=524288

[Install]
WantedBy=sockets.target
150 changes: 94 additions & 56 deletions DnsServerCore/Dns/DnsServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6131,64 +6131,99 @@ public async Task StartAsync(bool throwIfBindFails = false)

_state = ServiceState.Starting;

//bind on all local end points
foreach (IPEndPoint localEP in _localEndPoints)
//check for systemd socket activation
bool useSystemdSockets = false;

if (Environment.OSVersion.Platform == PlatformID.Unix)
{
Socket udpListener = null;
IReadOnlyList<Socket> systemdSockets = SystemdSocketActivation.GetSockets();

try
if (systemdSockets.Count > 0)
{
udpListener = new Socket(localEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);

#region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom()
useSystemdSockets = true;

if (Environment.OSVersion.Platform == PlatformID.Win32NT)
foreach (Socket socket in systemdSockets)
{
const uint IOC_IN = 0x80000000;
const uint IOC_VENDOR = 0x18000000;
const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
if (socket.SocketType == SocketType.Dgram)
{
socket.ReceiveBufferSize = 512 * 1024;
socket.SendBufferSize = 512 * 1024;

udpListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
}
_udpListeners.Add(socket);

#endregion
_log.Write((IPEndPoint)socket.LocalEndPoint, DnsTransportProtocol.Udp, "DNS Server was bound successfully (systemd socket activation).");
}
else if (socket.SocketType == SocketType.Stream)
{
_tcpListeners.Add(socket);

if (Environment.OSVersion.Platform == PlatformID.Unix)
udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses
_log.Write((IPEndPoint)socket.LocalEndPoint, DnsTransportProtocol.Tcp, "DNS Server was bound successfully (systemd socket activation).");
}
}
}
}

udpListener.ReceiveBufferSize = 512 * 1024;
udpListener.SendBufferSize = 512 * 1024;
//bind on all local end points
foreach (IPEndPoint localEP in _localEndPoints)
{
if (!useSystemdSockets)
{
Socket udpListener = null;

try
{
udpListener.Bind(localEP);
}
catch (SocketException ex1)
{
switch (ex1.ErrorCode)
udpListener = new Socket(localEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);

#region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom()

if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
case 99: //SocketException (99): Cannot assign requested address
await Task.Delay(5000); //wait for address to be available before retrying
udpListener.Bind(localEP);
break;
const uint IOC_IN = 0x80000000;
const uint IOC_VENDOR = 0x18000000;
const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;

default:
throw;
udpListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
}
}

_udpListeners.Add(udpListener);
#endregion

_log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString());
if (Environment.OSVersion.Platform == PlatformID.Unix)
udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses

udpListener?.Dispose();
udpListener.ReceiveBufferSize = 512 * 1024;
udpListener.SendBufferSize = 512 * 1024;

if (throwIfBindFails)
throw;
try
{
udpListener.Bind(localEP);
}
catch (SocketException ex1)
{
switch (ex1.ErrorCode)
{
case 99: //SocketException (99): Cannot assign requested address
await Task.Delay(5000); //wait for address to be available before retrying
udpListener.Bind(localEP);
break;

default:
throw;
}
}

_udpListeners.Add(udpListener);

_log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString());

udpListener?.Dispose();

if (throwIfBindFails)
throw;
}
}

if (_enableDnsOverUdpProxy)
Expand Down Expand Up @@ -6236,30 +6271,33 @@ public async Task StartAsync(bool throwIfBindFails = false)
}
}

Socket tcpListener = null;

try
if (!useSystemdSockets)
{
tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
Socket tcpListener = null;

if (Environment.OSVersion.Platform == PlatformID.Unix)
tcpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses
try
{
tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

tcpListener.Bind(localEP);
tcpListener.Listen(_listenBacklog);
if (Environment.OSVersion.Platform == PlatformID.Unix)
tcpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses

_tcpListeners.Add(tcpListener);
tcpListener.Bind(localEP);
tcpListener.Listen(_listenBacklog);

_log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString());
_tcpListeners.Add(tcpListener);

tcpListener?.Dispose();
_log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully.");
}
catch (Exception ex)
{
_log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString());

if (throwIfBindFails)
throw;
tcpListener?.Dispose();

if (throwIfBindFails)
throw;
}
}

if (_enableDnsOverTcpProxy)
Expand Down
61 changes: 61 additions & 0 deletions DnsServerCore/Dns/SystemdSocketActivation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
Technitium DNS Server
Copyright (C) 2025 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 System;
using System.Collections.Generic;
using System.Net.Sockets;

namespace DnsServerCore.Dns
{
static class SystemdSocketActivation
{
const int SD_LISTEN_FDS_START = 3;

public static IReadOnlyList<Socket> GetSockets()
{
string listenPid = Environment.GetEnvironmentVariable("LISTEN_PID");
string listenFds = Environment.GetEnvironmentVariable("LISTEN_FDS");

if (listenPid is null || listenFds is null)
return [];

if (!int.TryParse(listenPid, out int pid) || pid != Environment.ProcessId)
return [];

if (!int.TryParse(listenFds, out int count) || count <= 0)
return [];

//unset variables so child processes don't inherit them
Environment.SetEnvironmentVariable("LISTEN_PID", null);
Environment.SetEnvironmentVariable("LISTEN_FDS", null);
Environment.SetEnvironmentVariable("LISTEN_FDNAMES", null);

List<Socket> sockets = new List<Socket>(count);

for (int i = 0; i < count; i++)
{
int fd = SD_LISTEN_FDS_START + i;
SafeSocketHandle handle = new SafeSocketHandle(new IntPtr(fd), ownsHandle: true);
sockets.Add(new Socket(handle));
}

return sockets;
}
}
}