This is an automated email from the ASF dual-hosted git repository. FreeAndNil pushed a commit to branch Feature/security-audit-hardening in repository https://gitbox.apache.org/repos/asf/logging-log4net.git
commit 15d16eedf5804c5b441db52096d4ee018a4e5028 Author: Jan Friedrich <[email protected]> AuthorDate: Mon Aug 17 23:56:00 2026 +0200 add a listen address to TelnetAppender The listening socket was bound to IPAddress.Any with no way to scope it, so an operator who only wanted to watch the log from the machine itself still got a listener on every interface. ListenAddress fills that gap. The default is unchanged, so nothing moves unless it is set: the connecting client is trusted, as the manual now states, and flipping the default would break every remote monitoring setup on upgrade. The listening socket now takes its family from the address rather than always being InterNetwork, so an IPv6 address works too. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- src/changelog/3.4.0/309-telnet-listen-address.xml | 13 ++++++ src/log4net.Tests/Appender/TelnetAppenderTest.cs | 52 ++++++++++++++++++++++ src/log4net/Appender/TelnetAppender.cs | 47 +++++++++++++++++-- .../configuration/appenders/telnetappender.adoc | 16 +++++-- 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/changelog/3.4.0/309-telnet-listen-address.xml b/src/changelog/3.4.0/309-telnet-listen-address.xml new file mode 100644 index 00000000..05853417 --- /dev/null +++ b/src/changelog/3.4.0/309-telnet-listen-address.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="https://logging.apache.org/xml/ns" + xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" + type="added"> + <issue id="309" link="https://github.com/apache/logging-log4net/pull/309"/> + <description format="asciidoc"> + add `ListenAddress` to `TelnetAppender`. The listening socket was bound to `IPAddress.Any` with + no way to scope it, so an operator who only wanted to watch the log from the machine itself still + got a listener on every interface. The default is unchanged, and the listening socket now follows + the address family, so an IPv6 address works too (audit 1231d72-f002) + </description> +</entry> diff --git a/src/log4net.Tests/Appender/TelnetAppenderTest.cs b/src/log4net.Tests/Appender/TelnetAppenderTest.cs index e5c238c3..5d8f37cd 100644 --- a/src/log4net.Tests/Appender/TelnetAppenderTest.cs +++ b/src/log4net.Tests/Appender/TelnetAppenderTest.cs @@ -27,6 +27,7 @@ using log4net.Appender; using log4net.Config; using log4net.Core; +using log4net.Layout; using log4net.Repository; using log4net.Tests.Appender.Internal; using NUnit.Framework; @@ -156,6 +157,57 @@ public void SendTimeoutMillisRejectsNegativeValuesButAllowsZero() Assert.That(appender.SendTimeoutMillis, Is.EqualTo(250)); } + /// <summary> + /// The appender accepts connections on every interface unless told otherwise, which is the + /// behaviour it has always had. + /// </summary> + [Test] + public void ListenAddressDefaultsToEveryInterface() + => Assert.That(new TelnetAppender().ListenAddress, Is.EqualTo(IPAddress.Any)); + + /// <summary> + /// Binding to the loopback address has to keep the port unreachable from other machines, which + /// is what an operator asking for it wants. + /// </summary> + [Test] + [NonParallelizable] + public void ListenAddressBindsOnlyThatAddress() + { + int port = FindFreeTcpPort(); + TelnetAppender appender = new() + { + Port = port, + ListenAddress = IPAddress.Loopback, + Layout = new PatternLayout("%message%newline") + }; + appender.ActivateOptions(); + try + { + // The loopback listener accepts a loopback connection. + using (Socket loopback = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + loopback.Connect(new IPEndPoint(IPAddress.Loopback, port)); + Assert.That(loopback.Connected, Is.True); + } + + // and nothing is listening on the machine's other addresses + IPAddress? routable = Array.Find( + Dns.GetHostAddresses(Dns.GetHostName()), + address => address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(address)); + if (routable is null) + { + Assert.Ignore("no non-loopback IPv4 address on this machine to test against"); + } + + using Socket external = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + Assert.That(() => external.Connect(new IPEndPoint(routable!, port)), Throws.TypeOf<SocketException>()); + } + finally + { + appender.Close(); + } + } + /// <summary> /// Asks the OS for a currently unused TCP port - a fixed port would collide with /// other tests or processes on the build machine. diff --git a/src/log4net/Appender/TelnetAppender.cs b/src/log4net/Appender/TelnetAppender.cs index e62e9346..a0b04e13 100644 --- a/src/log4net/Appender/TelnetAppender.cs +++ b/src/log4net/Appender/TelnetAppender.cs @@ -55,6 +55,28 @@ public class TelnetAppender : AppenderSkeleton private SocketHandler? _handler; private int _listeningPort = 23; private int _sendTimeoutMillis = 5_000; + private IPAddress _listenAddress = IPAddress.Any; + + /// <summary> + /// Gets or sets the address to listen on. + /// </summary> + /// <value> + /// The local address to accept connections on. The default is <see cref="IPAddress.Any"/>, every + /// interface of the machine. + /// </value> + /// <remarks> + /// <para> + /// Set this to <see cref="IPAddress.Loopback"/> to accept connections only from the machine the + /// application runs on, which is what the diagnostic use this appender is meant for usually + /// needs. + /// </para> + /// </remarks> + /// <exception cref="ArgumentNullException">The value specified is <see langword="null"/>.</exception> + public IPAddress ListenAddress + { + get => _listenAddress; + set => _listenAddress = value.EnsureNotNull(); + } /// <summary> /// The fully qualified type of the TelnetAppender class. @@ -158,8 +180,8 @@ public override void ActivateOptions() base.ActivateOptions(); try { - LogLog.Debug(_declaringType, $"Creating SocketHandler to listen on port [{_listeningPort}]"); - _handler = new SocketHandler(_listeningPort, _sendTimeoutMillis); + LogLog.Debug(_declaringType, $"Creating SocketHandler to listen on [{_listenAddress}]:[{_listeningPort}]"); + _handler = new SocketHandler(_listenAddress, _listeningPort, _sendTimeoutMillis); } catch (Exception ex) { @@ -303,10 +325,27 @@ public SocketHandler(int port) /// </para> /// </remarks> public SocketHandler(int port, int sendTimeoutMillis) + : this(IPAddress.Any, port, sendTimeoutMillis) + { } + + /// <summary> + /// Opens a new server port on <paramref ref="port"/> of <paramref ref="listenAddress"/> + /// </summary> + /// <param name="listenAddress">the local address to accept connections on</param> + /// <param name="port">the local port to listen on for connections</param> + /// <param name="sendTimeoutMillis">the time, in milliseconds, that a write to a client may + /// block before that client is disconnected, or 0 to block indefinitely</param> + /// <remarks> + /// <para> + /// Creates a socket handler on the specified local address and server port. + /// </para> + /// </remarks> + public SocketHandler(IPAddress listenAddress, int port, int sendTimeoutMillis) { _sendTimeoutMillis = sendTimeoutMillis; - _serverSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - _serverSocket.Bind(new IPEndPoint(IPAddress.Any, port)); + // The address decides the family, so that an IPv6 address does not end up on an IPv4 socket. + _serverSocket = new(listenAddress.EnsureNotNull().AddressFamily, SocketType.Stream, ProtocolType.Tcp); + _serverSocket.Bind(new IPEndPoint(listenAddress, port)); _serverSocket.Listen(5); AcceptConnection(); } diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/telnetappender.adoc b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/telnetappender.adoc index 6ce35769..6751ca0c 100644 --- a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/telnetappender.adoc +++ b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/telnetappender.adoc @@ -33,6 +33,7 @@ The following example configures the appender to listen on port 8023. [source,xml] ---- <appender name="TelnetAppender" type="log4net.Appender.TelnetAppender"> + <listenAddress value="127.0.0.1" /> <port value="8023" /> <sendTimeoutMillis value="5000" /> <layout type="log4net.Layout.PatternLayout"> @@ -48,6 +49,14 @@ The following example configures the appender to listen on port 8023. The TCP port to listen on. The default is `23`, the telnet port. +`listenAddress`:: +The local address to accept connections on. +The default is `0.0.0.0`, every interface of the machine. ++ +Set it to `127.0.0.1` to accept connections only from the machine the application runs on, which is +what diagnostic use usually needs. +An IPv6 address may be given instead, and the listening socket follows its family. + `sendTimeoutMillis`:: How long, in milliseconds, a write to a client may block before that client is treated as dead and disconnected. @@ -73,15 +82,16 @@ The appender therefore performs no authentication of its own. [WARNING] ==== -The connection is *unauthenticated* and *unencrypted*, and the appender listens on *all network -interfaces*. -There is no option to restrict the listen address, require a credential, or enable TLS. +The connection is *unauthenticated* and *unencrypted*, and by default the appender listens on *all +network interfaces*. +There is no option to require a credential or to enable TLS. Any client that can reach the port receives the full rendered log stream, including whatever the layout renders: user names, session identifiers, request parameters, stack traces. Keeping untrusted parties away from the port is the operator's responsibility, exactly as it is for a log file: +* Set `listenAddress` to `127.0.0.1` unless clients on other machines really have to connect. * Only enable this appender on a trusted network. * Restrict access to the port with a host firewall or network policy. * Prefer it for local or short-lived diagnostics rather than as a permanent logging destination.
