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.

Reply via email to