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; + } + } +}