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:
ImplicitTlsSmtpStartTlsImapStartTlsPop3StartTlsRdpTls
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:
25usually means SMTP STARTTLS143usually means IMAP STARTTLS110usually means POP3 STLS3389usually means RDP security negotiation before TLS443usually means implicit TLS
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:
PortTransportTransportSpecified
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:
- common ports infer the expected transport
- explicit transport values override inference
- unknown ports fall back to the provided transport
Get-TLSleuthCertificate.Transport.Tests.ps1 verifies public command behavior:
- port
3389infersRdpTlswhen-Transportis omitted - explicit
ImplicitTlson port3389does not call the RDP negotiation path
Test-TLSleuthProtocol.Tests.ps1 verifies the same inference path for protocol
testing:
- port
587infersSmtpStartTls - each protocol attempt uses the inferred transport
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:
- the service uses a non-standard port
- a load balancer or port-forwarding rule changes the expected protocol
- you are testing a deliberately unusual endpoint
- you want to confirm behavior with a specific transport path
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:
- omit
-Transporton common ports and TLSleuth chooses the expected transport - supply
-Transportwhen you need exact control - existing output contracts remain unchanged
Internally, the feature stays small: one resolver helper, one operation-context update, and focused tests around inference, explicit overrides, and public command routing.