Skip to content

Conversation

@carlhoerberg
Copy link
Contributor

@carlhoerberg carlhoerberg commented Nov 29, 2025

Summary

  • Adds on_server_name method to OpenSSL::SSL::Context::Server for Server Name Indication (SNI) support
  • Allows TLS servers to present different certificates based on the client's requested hostname
  • Enables virtual hosting over TLS with a single listening socket

Example

default_context = OpenSSL::SSL::Context::Server.new
default_context.certificate_chain = "default.crt"
default_context.private_key = "default.key"

example_context = OpenSSL::SSL::Context::Server.new
example_context.certificate_chain = "example.com.crt"
example_context.private_key = "example.com.key"

default_context.on_server_name do |hostname|
  case hostname
  when "example.com", "www.example.com"
    example_context
  else
    nil # use default context
  end
end

Test plan

  • Added spec for on_server_name method in context_spec.cr
  • Added end-to-end spec in socket_spec.cr that verifies the callback receives the client hostname

🤖 Generated with Claude Code

carlhoerberg and others added 2 commits November 29, 2025 09:47
This adds Server Name Indication (SNI) callback support, allowing TLS servers
to present different certificates based on the hostname the client is connecting to.

The `set_sni_callback` method accepts a block that receives the client's requested
hostname and returns either a configured `SSL::Context::Server` for that hostname,
or `nil` to continue using the default context.

Example:
```crystal
default_context.set_sni_callback do |hostname|
  case hostname
  when "example.com"
    example_context
  else
    nil
  end
end
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Also use null pointer instead of nilable type for callback box,
matching the existing pattern for @alpn_protocol.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@carlhoerberg carlhoerberg changed the title Add SNI callback support to OpenSSL::SSL::Context::Server Add SNI support via on_server_name callback to OpenSSL::SSL::Context::Server Nov 29, 2025
carlhoerberg and others added 5 commits November 30, 2025 21:17
Co-authored-by: Johannes Müller <[email protected]>
Co-authored-by: Johannes Müller <[email protected]>
Let exceptions propagate naturally instead of silently swallowing them.
This is consistent with other OpenSSL callbacks in the same file
(set_cert_verify_callback, alpn_protocol=) which don't have exception
handling either.

Exceptions will bubble up through the libssl stack and surface at the
SSL_accept call, where they can be properly handled by Crystal code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
C callbacks must always return an error code, never raise - letting
exceptions unwind through the C stack is unsafe as libssl cannot do
its cleanup.

On any exception in the SNI callback, set the alert pointer to
SSL_AD_INTERNAL_ERROR and return SSL_TLSEXT_ERR_ALERT_FATAL. This
lets OpenSSL handle the error properly and terminate the handshake
with an appropriate alert.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Verifies that when the SNI callback raises an exception, the server
returns an SSL error (via ALERT_FATAL) rather than crashing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Co-authored-by: Julien Portalier <[email protected]>
@straight-shoota
Copy link
Member

Apparently this is broken in the interpreter. Perhaps we could just skip the spec in interpreted mode? With a note that this leads to invalid memory access.

And Crystal 1.0 doesn't seem to support the proc literal syntax. Might be best to use Proc.new instead: Proc(LibSSL::SSL, LibC::Int*, Void*, LibC::Int).new { |ssl, alert_ptr, arg|

@ysbaddaden ysbaddaden added this to the 1.19.0 milestone Dec 4, 2025
Co-authored-by: Sijawusz Pur Rahnama <[email protected]>
Copy link
Collaborator

@ysbaddaden ysbaddaden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a polish change to make sure the c callback is properly validated by the compiler (no closure data) when sent to the OpenSSL function.

This commit corrects the type signature for the SSL_CTX_callback_ctrl C binding to prevent memory safety issues related to garbage collection and closures.

Co-authored-by: Julien Portalier <[email protected]>
@straight-shoota straight-shoota changed the title Add SNI support via on_server_name callback to OpenSSL::SSL::Context::Server Add OpenSSL::SSL::Context::Server#on_server_name for SNI Dec 8, 2025
@straight-shoota straight-shoota merged commit e80f29f into crystal-lang:master Dec 10, 2025
47 of 48 checks passed
@straight-shoota
Copy link
Member

I have some ideas for improving this feature: #16494

@carlhoerberg
Copy link
Contributor Author

We've started to notice GC Warning: Finalization cycle involving (???) when running SNI specs. We suspect it's related to the storage of the callback, @sni_callback_box. One way we can avoid it is to use a class level hash for storing the callback box, and deleting it in the finalizer, but that seems less than optimal..

@ysbaddaden
Copy link
Collaborator

I assume the issue comes from the captured block closure: it can have direct references to other contexts and trying to finalize one context leads the GC to identify a circular finalization... which isn't an issue because the finalizer only accesses the object itself.

I wish there was a way to tell the GC to skip the check (unsafe finalizer).

@carlhoerberg
Copy link
Contributor Author

I wish there was a way to tell the GC to skip the check (unsafe finalizer).

how about GC_register_finalizer_ignore_self? https://github.com/bdwgc/bdwgc/blob/master/docs/finalization.md#getting-around-topological-finalization-ordering

@straight-shoota
Copy link
Member

We already use GC_register_finalizer_ignore_self for all finalizers:

crystal/src/gc/boehm.cr

Lines 338 to 343 in 37cdfa7

private def self.add_finalizer_impl(object : T) forall T
LibGC.register_finalizer_ignore_self(object.as(Void*),
->(obj, data) { obj.as(T).finalize },
nil, nil, nil)
nil
end

jage added a commit to cloudamqp/lavinmq that referenced this pull request Jan 16, 2026
Crystal 1.19.0 includes it, which cause a conflict:

    In src/stdlib/openssl_sni.cr:13:3

     13 | SSL_CTRL_SET_TLSEXT_SERVERNAME_CB  = 53
          ^--------------------------------
    Error: already initialized constant LibSSL::SSL_CTRL_SET_TLSEXT_SERVERNAME_CB

Replace custom openssl_sni.cr monkey-patch with the new
on_server_name callback added in crystal-lang/crystal#16452 by us

This has only been tested by existing specs.
jage added a commit to cloudamqp/lavinmq that referenced this pull request Jan 16, 2026
Crystal 1.19.0 includes it, which cause a conflict:

    In src/stdlib/openssl_sni.cr:13:3

     13 | SSL_CTRL_SET_TLSEXT_SERVERNAME_CB  = 53
          ^--------------------------------
    Error: already initialized constant LibSSL::SSL_CTRL_SET_TLSEXT_SERVERNAME_CB

Replace custom openssl_sni.cr monkey-patch with the new
on_server_name callback added in crystal-lang/crystal#16452 by us

This has only been tested by existing specs.
jage added a commit to cloudamqp/lavinmq that referenced this pull request Jan 16, 2026
Crystal 1.19.0 includes it, which cause a conflict:

    In src/stdlib/openssl_sni.cr:13:3

     13 | SSL_CTRL_SET_TLSEXT_SERVERNAME_CB  = 53
          ^--------------------------------
    Error: already initialized constant LibSSL::SSL_CTRL_SET_TLSEXT_SERVERNAME_CB

Replace custom openssl_sni.cr monkey-patch with the new
on_server_name callback added in crystal-lang/crystal#16452 by us

This has only been tested by existing specs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants