Skip to the content.

TLSleuth supports several ways to reach a TLS handshake. Some services begin TLS immediately after the TCP connection is opened. Other services require a plaintext or binary negotiation step first.

That distinction is captured by the -Transport parameter:

Before this change, callers needed to know and supply the right transport for explicit TLS services. That is still supported, but TLSleuth can now infer the transport from common port numbers when -Transport is omitted.

The Problem

The old behavior was technically clear but not always ergonomic. A user could run this:

Get-TLSleuthCertificate -Hostname smtp.gmail.com -Port 587 -Transport SmtpStartTls

That is explicit and unambiguous. But port 587 is commonly SMTP submission, where STARTTLS is expected. Similarly:

TLSleuth already had the transport implementations. The missing piece was a safe way to choose the expected transport when the user had not made an explicit choice.

What Changed

TLSleuth now maps common ports to supported transports:

Port Inferred transport
25 SmtpStartTls
110 Pop3StartTls
143 ImapStartTls
443 ImplicitTls
587 SmtpStartTls
3389 RdpTls

This only happens when -Transport is omitted. If the caller supplies -Transport, TLSleuth uses that value even when the port number would normally suggest something else.

That rule matters for custom deployments. A service can run on a non-standard port, and a port can be forwarded to a service that does not match the usual convention. Explicit user input should always win.

There are no new public parameters, no new output properties, and no breaking changes to the public result contracts.

How It Works Internally

The implementation is intentionally small. A new private helper, Resolve-TlsTransport, owns the port-to-transport map:

$transportByPort = @{
    25   = 'SmtpStartTls'
    110  = 'Pop3StartTls'
    143  = 'ImapStartTls'
    443  = 'ImplicitTls'
    587  = 'SmtpStartTls'
    3389 = 'RdpTls'
}

The resolver takes three inputs:

If TransportSpecified is set, the helper returns the supplied transport unchanged. Otherwise, it checks the common-port map and falls back to the provided transport when the port is unknown.

Preserving Explicit Overrides

The main PowerShell detail is that both public commands still declare:

[string]$Transport = 'ImplicitTls'

That default is useful, but it means the private helper cannot tell by looking at $Transport alone whether the user explicitly supplied -Transport ImplicitTls or simply omitted the parameter.

The public commands solve that at the boundary by checking $PSBoundParameters:

-TransportSpecified:($PSBoundParameters.ContainsKey('Transport'))

That value is passed into New-TlsOperationContext, which calls Resolve-TlsTransport and stores the effective transport in the shared command context.

The rest of the pipeline does not need to know whether the transport was explicit or inferred. It just uses $context.Transport.

Updated Flow

The command flow now looks like this:

flowchart TD
    User["User command"]
    Bound["Was -Transport bound?"]
    Context["New-TlsOperationContext"]
    Resolve["Resolve-TlsTransport"]
    Options["New-TlsTransportOptionSet"]
    Connect["New-TlsConnectionContext"]
    Dispatch["Invoke-TlsTransportNegotiation"]
    Handshake["Start-TlsHandshake"]
    Result["TLSleuth result object"]

    User --> Bound
    User --> Context
    Bound --> Context
    Context --> Resolve
    Resolve --> Options
    Context --> Connect
    Context --> Dispatch
    Dispatch --> Handshake
    Handshake --> Result

The important part is where inference happens. It is part of operation-context normalization, before TCP connection setup and before transport negotiation. That keeps public command orchestration simple and avoids duplicating the port map in multiple places.

Example Usage

HTTPS on port 443 still works without specifying a transport:

Get-TLSleuthCertificate -Hostname github.com -Port 443

SMTP STARTTLS on port 587 can now omit -Transport:

Get-TLSleuthCertificate -Hostname smtp.gmail.com -Port 587

IMAP STARTTLS on port 143:

Get-TLSleuthCertificate -Hostname outlook.office365.com -Port 143

POP3 STLS on port 110:

Get-TLSleuthCertificate -Hostname pop.gmail.com -Port 110

RDP on port 3389:

Get-TLSleuthCertificate -Hostname rdp.example.com -Port 3389

Explicit transport still overrides inference:

Get-TLSleuthCertificate `
  -Hostname rdp.example.com `
  -Port 3389 `
  -Transport ImplicitTls

That last example is intentionally allowed. It may fail if the endpoint is a normal RDP service, but the user made an explicit choice and TLSleuth honors it.

The same inference behavior is used by Test-TLSleuthProtocol:

Test-TLSleuthProtocol -Hostname smtp.gmail.com -Port 587 |
  Select Protocol, ConnectionSuccessful, Transport, ErrorMessage

Testing

The change is covered in three focused areas.

New-TlsOperationContext.Tests.ps1 verifies the resolver behavior:

Get-TLSleuthCertificate.Transport.Tests.ps1 verifies public command behavior:

Test-TLSleuthProtocol.Tests.ps1 verifies the same inference path for protocol testing:

The focused verification command was:

Invoke-Pester -Path `
  .\source\tests\unit\New-TlsOperationContext.Tests.ps1, `
  .\source\tests\unit\Get-TLSleuthCertificate.Transport.Tests.ps1, `
  .\source\tests\unit\Test-TLSleuthProtocol.Tests.ps1

The full validation command was:

Invoke-Build Validate -File .\TLSleuth.build.ps1

At the time this was implemented, the focused test set passed with 27 tests, and the full validation pass completed analyzer, release metadata, 101 unit tests, and build successfully.

Operational Considerations

Port inference is a convenience, not service discovery. TLSleuth does not probe the server to identify the service before choosing the transport. It uses a fixed map of common ports.

That is why explicit -Transport remains important:

Get-TLSleuthCertificate -Hostname mail.example.com -Port 2525 -Transport SmtpStartTls

Use explicit transport selection when:

For RDP, inferred RdpTls still depends on the server accepting TLS-capable RDP security negotiation. If the server rejects negotiation before TLS starts, no TLS certificate is available from that path.

Summary

Common-port transport inference makes TLSleuth easier to use without weakening its explicit control model.

The user-facing behavior is simple:

Internally, the feature stays small: one resolver helper, one operation-context update, and focused tests around inference, explicit overrides, and public command routing.