diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 000000000..e3fb51d39
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -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 }}
diff --git a/DnsServerApp/DnsServerApp.csproj b/DnsServerApp/DnsServerApp.csproj
index 4991224d9..d74fd5d10 100644
--- a/DnsServerApp/DnsServerApp.csproj
+++ b/DnsServerApp/DnsServerApp.csproj
@@ -33,6 +33,9 @@
PreserveNewest
+
+ PreserveNewest
+
diff --git a/DnsServerApp/systemd.socket b/DnsServerApp/systemd.socket
new file mode 100644
index 000000000..4ca80c4f9
--- /dev/null
+++ b/DnsServerApp/systemd.socket
@@ -0,0 +1,11 @@
+[Unit]
+Description=Technitium DNS Server Socket
+
+[Socket]
+ListenDatagram=53
+ListenStream=53
+ReceiveBuffer=524288
+SendBuffer=524288
+
+[Install]
+WantedBy=sockets.target
diff --git a/DnsServerCore/Dns/DnsServer.cs b/DnsServerCore/Dns/DnsServer.cs
index ffbfbf8db..85eca63e1 100644
--- a/DnsServerCore/Dns/DnsServer.cs
+++ b/DnsServerCore/Dns/DnsServer.cs
@@ -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 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)
@@ -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)
diff --git a/DnsServerCore/Dns/SystemdSocketActivation.cs b/DnsServerCore/Dns/SystemdSocketActivation.cs
new file mode 100644
index 000000000..c03c1a1f0
--- /dev/null
+++ b/DnsServerCore/Dns/SystemdSocketActivation.cs
@@ -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 .
+
+*/
+
+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 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 sockets = new List(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;
+ }
+ }
+}