Files
MQTTnet/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs
T
Alexander KöpkeandCopilot 68da92fd4b Add support for pluggable stream provider to enable SOCKS5 proxies (#2251)
* Add SOCKS5 proxy support via pluggable stream provider

Introduces MQTTnet.Extensions.Socks5, enabling SOCKS5 proxy support for MQTTnet TCP transport. Adds IMqttClientStreamProvider interface, Socks5StreamProvider implementation, and related configuration options. Updates MqttTcpChannel to support stream providers and TLS layering. Includes unit tests with a fake SOCKS5 server and usage samples. Solution and project references updated accordingly.

* Add SOCKS5 options validation for invalid configurations

* Tighten SOCKS5 RFC compliance checks

---------

Co-authored-by: Copilot <Copilot@users.noreply.github.com>
2026-06-18 17:51:52 +02:00

536 lines
17 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using MQTTnet.Channel;
using MQTTnet.Formatter;
using MQTTnet.Packets;
using MQTTnet.Protocol;
namespace MQTTnet;
public sealed class MqttClientOptionsBuilder
{
readonly MqttClientOptions _options = new();
int? _port;
EndPoint _remoteEndPoint;
MqttClientTcpOptions _tcpOptions;
MqttClientTlsOptions _tlsOptions;
MqttClientWebSocketOptions _webSocketOptions;
public MqttClientOptions Build()
{
if (_tcpOptions == null && _webSocketOptions == null)
{
throw new InvalidOperationException("A channel must be set.");
}
// The user can specify the TCP options with already configured TLS options
// or start with TLS settings not knowing which transport will be used (depending
// on the order of called methods from the builder).
// The builder prefers the explicitly set TLS options!
var tlsOptions = _tlsOptions ?? _tcpOptions?.TlsOptions;
if (_tcpOptions != null)
{
_tcpOptions.TlsOptions = tlsOptions;
if (_remoteEndPoint == null)
{
throw new ArgumentException("No endpoint is set.");
}
if (_remoteEndPoint is DnsEndPoint dns)
{
if (dns.Port == 0)
{
if (_port.HasValue)
{
_remoteEndPoint = new DnsEndPoint(dns.Host, _port.Value, dns.AddressFamily);
}
else
{
_remoteEndPoint = new DnsEndPoint(dns.Host, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure, dns.AddressFamily);
}
}
}
if (_remoteEndPoint is IPEndPoint ip)
{
if (ip.Port == 0)
{
if (_port.HasValue)
{
_remoteEndPoint = new IPEndPoint(ip.Address, _port.Value);
}
else
{
_remoteEndPoint = new IPEndPoint(ip.Address, tlsOptions?.UseTls == false ? MqttPorts.Default : MqttPorts.Secure);
}
}
}
if (_tcpOptions.RemoteEndpoint == null)
{
_tcpOptions.RemoteEndpoint = _remoteEndPoint;
}
}
else if (_webSocketOptions != null)
{
_webSocketOptions.TlsOptions = tlsOptions;
}
_options.ChannelOptions = (IMqttClientChannelOptions)_tcpOptions ?? _webSocketOptions;
MqttClientOptionsValidator.ThrowIfNotSupported(_options);
return _options;
}
public MqttClientOptionsBuilder WithAddressFamily(AddressFamily addressFamily)
{
_tcpOptions.AddressFamily = addressFamily;
return this;
}
/// <summary>
/// Clean session is used in MQTT versions below 5.0.0. It is the same as setting "CleanStart".
/// </summary>
public MqttClientOptionsBuilder WithCleanSession(bool value = true)
{
_options.CleanSession = value;
return this;
}
/// <summary>
/// Clean start is used in MQTT versions 5.0.0 and higher. It is the same as setting "CleanSession".
/// </summary>
public MqttClientOptionsBuilder WithCleanStart(bool value = true)
{
_options.CleanSession = value;
return this;
}
public MqttClientOptionsBuilder WithClientId(string value)
{
_options.ClientId = value;
return this;
}
public MqttClientOptionsBuilder WithConnectionUri(Uri uri)
{
ArgumentNullException.ThrowIfNull(uri);
var port = uri.IsDefaultPort ? null : (int?)uri.Port;
switch (uri.Scheme.ToLowerInvariant())
{
case "tcp":
case "mqtt":
WithTcpServer(uri.Host, port).WithAddressFamily(AddressFamily.Unspecified).WithProtocolType(ProtocolType.Tcp).WithTlsOptions(o => o.UseTls(false));
break;
case "mqtts":
WithTcpServer(uri.Host, port).WithAddressFamily(AddressFamily.Unspecified).WithProtocolType(ProtocolType.Tcp).WithTlsOptions(o => o.UseTls());
break;
case "ws":
WithWebSocketServer(o => o.WithUri(uri.ToString())).WithTlsOptions(o => o.UseTls(false));
break;
case "wss":
WithWebSocketServer(o => o.WithUri(uri.ToString())).WithTlsOptions(o => o.UseTls());
break;
// unix:///path/to/socket
case "unix":
WithEndPoint(new UnixDomainSocketEndPoint(uri.AbsolutePath))
.WithAddressFamily(AddressFamily.Unix)
.WithProtocolType(ProtocolType.Unspecified)
.WithTlsOptions(o => o.UseTls(false));
break;
default:
throw new ArgumentException("Unexpected scheme in uri.");
}
if (!string.IsNullOrEmpty(uri.UserInfo))
{
var userInfo = uri.UserInfo.Split(':');
var username = userInfo[0];
var password = userInfo.Length > 1 ? userInfo[1] : "";
WithCredentials(username, password);
}
return this;
}
public MqttClientOptionsBuilder WithConnectionUri(string uri)
{
return WithConnectionUri(new Uri(uri, UriKind.Absolute));
}
public MqttClientOptionsBuilder WithCredentials(string username, string password)
{
byte[] passwordBuffer = null;
if (password != null)
{
passwordBuffer = Encoding.UTF8.GetBytes(password);
}
return WithCredentials(username, passwordBuffer);
}
public MqttClientOptionsBuilder WithCredentials(string username, byte[] password = null)
{
return WithCredentials(new MqttClientCredentials(username, password));
}
public MqttClientOptionsBuilder WithCredentials(IMqttClientCredentialsProvider credentials)
{
_options.Credentials = credentials;
return this;
}
public MqttClientOptionsBuilder WithEndPoint(EndPoint endPoint)
{
_remoteEndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
_tcpOptions = new MqttClientTcpOptions();
return this;
}
public MqttClientOptionsBuilder WithEnhancedAuthentication(string method, byte[] data = null)
{
_options.AuthenticationMethod = method;
_options.AuthenticationData = data;
return this;
}
public MqttClientOptionsBuilder WithEnhancedAuthenticationHandler(IMqttEnhancedAuthenticationHandler handler)
{
_options.EnhancedAuthenticationHandler = handler;
return this;
}
public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan value)
{
_options.KeepAlivePeriod = value;
return this;
}
public MqttClientOptionsBuilder WithMaximumPacketSize(uint maximumPacketSize)
{
_options.MaximumPacketSize = maximumPacketSize;
return this;
}
public MqttClientOptionsBuilder WithNoKeepAlive()
{
return WithKeepAlivePeriod(TimeSpan.Zero);
}
/// <summary>
/// Usually the MQTT packets can be sent partially. This is done by using multiple TCP packets
/// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and
/// will close the connection when receiving such packets. If such a service is used this flag must
/// be set to _true_.
/// </summary>
public MqttClientOptionsBuilder WithoutPacketFragmentation()
{
_options.AllowPacketFragmentation = false;
return this;
}
public MqttClientOptionsBuilder WithProtocolType(ProtocolType protocolType)
{
_tcpOptions.ProtocolType = protocolType;
return this;
}
/// <summary>
/// Configures an optional <see cref="IMqttClientStreamProvider"/> on the TCP options.
/// The provider is responsible for opening the transport stream to the broker (e.g. through
/// a SOCKS5 proxy). When the connection should be TLS-secured, configure TLS via
/// <c>WithTlsOptions(...)</c>; TLS will be layered on top of the stream returned by the
/// provider, using the broker host as SNI.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when no TCP transport has been configured yet (call <c>WithTcpServer</c> or
/// <c>WithEndPoint</c> before this method).
/// </exception>
public MqttClientOptionsBuilder WithStreamProvider(IMqttClientStreamProvider provider)
{
ArgumentNullException.ThrowIfNull(provider);
if (_tcpOptions == null)
{
throw new InvalidOperationException(
"A TCP transport must be configured (via WithTcpServer or WithEndPoint) before assigning a stream provider.");
}
_tcpOptions.StreamProvider = provider;
return this;
}
public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value)
{
if (value == MqttProtocolVersion.Unknown)
{
throw new ArgumentException("Protocol version is invalid.");
}
_options.ProtocolVersion = value;
return this;
}
public MqttClientOptionsBuilder WithReceiveMaximum(ushort receiveMaximum)
{
_options.ReceiveMaximum = receiveMaximum;
return this;
}
public MqttClientOptionsBuilder WithRequestProblemInformation(bool requestProblemInformation = true)
{
_options.RequestProblemInformation = requestProblemInformation;
return this;
}
public MqttClientOptionsBuilder WithRequestResponseInformation(bool requestResponseInformation = true)
{
_options.RequestResponseInformation = requestResponseInformation;
return this;
}
public MqttClientOptionsBuilder WithSessionExpiryInterval(uint sessionExpiryInterval)
{
_options.SessionExpiryInterval = sessionExpiryInterval;
return this;
}
public MqttClientOptionsBuilder WithTcpServer(string host, int? port = null, AddressFamily addressFamily = AddressFamily.Unspecified)
{
ArgumentNullException.ThrowIfNull(host);
_tcpOptions = new MqttClientTcpOptions();
// The value 0 will be updated when building the options.
// This a backward compatibility feature.
_remoteEndPoint = new DnsEndPoint(host, port ?? 0, addressFamily);
_port = port;
return this;
}
public MqttClientOptionsBuilder WithTcpServer(Action<MqttClientTcpOptions> optionsBuilder)
{
ArgumentNullException.ThrowIfNull(optionsBuilder);
_tcpOptions = new MqttClientTcpOptions();
optionsBuilder(_tcpOptions);
return this;
}
/// <summary>
/// Sets the timeout which will be applied at socket level and internal operations.
/// The default value is the same as for sockets in .NET in general.
/// </summary>
public MqttClientOptionsBuilder WithTimeout(TimeSpan value)
{
_options.Timeout = value;
return this;
}
public MqttClientOptionsBuilder WithTlsOptions(MqttClientTlsOptions tlsOptions)
{
_tlsOptions = tlsOptions;
return this;
}
public MqttClientOptionsBuilder WithTlsOptions(Action<MqttClientTlsOptionsBuilder> configure)
{
ArgumentNullException.ThrowIfNull(configure);
var builder = new MqttClientTlsOptionsBuilder();
configure.Invoke(builder);
_tlsOptions = builder.Build();
return this;
}
public MqttClientOptionsBuilder WithTopicAliasMaximum(ushort topicAliasMaximum)
{
_options.TopicAliasMaximum = topicAliasMaximum;
return this;
}
/// <summary>
/// If set to true, the bridge will attempt to indicate to the remote broker that it is a bridge not an ordinary
/// client.
/// If successful, this means that loop detection will be more effective and that retained messages will be propagated
/// correctly.
/// Not all brokers support this feature so it may be necessary to set it to false if your bridge does not connect
/// properly.
/// </summary>
public MqttClientOptionsBuilder WithTryPrivate(bool value = true)
{
_options.TryPrivate = value;
return this;
}
[Obsolete("Please use more performance `WithUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttClientOptionsBuilder WithUserProperty(string name, string value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWebSocketServer(Action<MqttClientWebSocketOptionsBuilder> configure)
{
ArgumentNullException.ThrowIfNull(configure);
var webSocketOptionsBuilder = new MqttClientWebSocketOptionsBuilder();
configure.Invoke(webSocketOptionsBuilder);
_webSocketOptions = webSocketOptionsBuilder.Build();
return this;
}
public MqttClientOptionsBuilder WithWillContentType(string willContentType)
{
_options.WillContentType = willContentType;
return this;
}
public MqttClientOptionsBuilder WithWillCorrelationData(byte[] willCorrelationData)
{
_options.WillCorrelationData = willCorrelationData;
return this;
}
public MqttClientOptionsBuilder WithWillDelayInterval(uint willDelayInterval)
{
_options.WillDelayInterval = willDelayInterval;
return this;
}
public MqttClientOptionsBuilder WithWillMessageExpiryInterval(uint willMessageExpiryInterval)
{
_options.WillMessageExpiryInterval = willMessageExpiryInterval;
return this;
}
public MqttClientOptionsBuilder WithWillPayload(byte[] willPayload)
{
_options.WillPayload = willPayload;
return this;
}
public MqttClientOptionsBuilder WithWillPayload(ArraySegment<byte> willPayload)
{
if (willPayload.Count == 0)
{
_options.WillPayload = null;
return this;
}
_options.WillPayload = willPayload.ToArray();
return this;
}
public MqttClientOptionsBuilder WithWillPayload(string willPayload)
{
if (string.IsNullOrEmpty(willPayload))
{
return WithWillPayload((byte[])null);
}
_options.WillPayload = Encoding.UTF8.GetBytes(willPayload);
return this;
}
public MqttClientOptionsBuilder WithWillPayloadFormatIndicator(MqttPayloadFormatIndicator willPayloadFormatIndicator)
{
_options.WillPayloadFormatIndicator = willPayloadFormatIndicator;
return this;
}
public MqttClientOptionsBuilder WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel willQualityOfServiceLevel)
{
_options.WillQualityOfServiceLevel = willQualityOfServiceLevel;
return this;
}
public MqttClientOptionsBuilder WithWillResponseTopic(string willResponseTopic)
{
_options.WillResponseTopic = willResponseTopic;
return this;
}
public MqttClientOptionsBuilder WithWillRetain(bool willRetain = true)
{
_options.WillRetain = willRetain;
return this;
}
public MqttClientOptionsBuilder WithWillTopic(string willTopic)
{
_options.WillTopic = willTopic;
return this;
}
[Obsolete("Please use more performance `WithWillUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttClientOptionsBuilder WithWillUserProperty(string name, string value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWillUserProperty(string name, ReadOnlyMemory<byte> value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWillUserProperty(string name, ArraySegment<byte> value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}