Repository: clj-commons/aleph
Pull request #748 (2025-04-05 14:32:10)

Adopting an existing, pre-bound server-socket channel
The use-case is to pass a server-socket channel, e.g. obtained by calling JVM's `System/inheritedChannel`. In this case, Aleph/Netty is not responsible for opening the channel nor closing it. It should simply listen on it. In other words, maintaining the connection sockets is still Aleph/Netty's responsibility, but not maintaining the listening socket if it has been passed to it.

* In the server startup part, this has been implemented as per Netty's Norman Maurer's advice [found here](https://stackoverflow.com/a/50196956).

* In the server shutdown part, Aleph is not shutting down nor closing the passed socket, because in practice it's owned by a process that passed it to Aleph process, and it's that parent process' responsibility to reuse or close it after Aleph process exits.

In terms of validation and fallback, for comparison, [this used to be Jetty's approach to adopting `inheritedChannel` at version 8](https://github.com/jetty/jetty.project/blob/jetty-8.0.0.v20110901/jetty-server/src/main/java/org/eclipse/jetty/server/nio/InheritedChannelConnector.java).

* Jetty automatically falls back to opening its own socket (with a warning) if the `inheritedChannel` is not supported or not present. In Aleph implementation, it refuses to start the server if the channel type is not supported, or the socket is not bound, but it silently falls back to an own socket if the passed channel is `nil`. As a result, it's Aleph user's responsibility to trigger an error if it expects an 'inherited channel' which is missing. Still, the user can expect that Aleph will fail early if a channel was actually passed, but it cannot be used.

* Unlike Jetty, Aleph doesn't itself call `configureBlocking false` on the channel because [this is done by Netty during server startup](https://github.com/netty/netty/blob/46d6e164a2a46e2c6d802f84e0c24abd475bcd89/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java#L84).


Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 31de964..428d2d6 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1,2 @@
-* @arnaudgeiser @kingmob @dergutemoritz
+* @arnaudgeiser @dergutemoritz
 /deps/ @dergutemoritz
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8a9446b..edf97c4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -101,7 +101,7 @@ While it's tempting to reformat things left and right, this has the downside of
 
 ## Updating dependencies
 
-There’s some extra steps to test when updating dependencies:
+There are some extra steps to test when updating dependencies:
 
 1. After you've made your changes to `project.clj`, be sure to rerun the tests with `lein test`.
 
diff --git a/README.md b/README.md
index efeace3..00d5ef2 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ For HTTP client requests, Aleph models itself after [clj-http](https://github.co
 
 Aleph attempts to mimic the clj-http API and capabilities fully. It supports multipart/form-data requests, cookie stores, proxy servers and requests inspection with a few notable differences:
 
-* proxy configuration should be set for the connection when seting up a connection pool, per-request proxy setups are not allowed
+* proxy configuration should be set for the connection when setting up a connection pool, per-request proxy setups are not allowed
 
 * HTTP proxy functionality is extended with tunneling settings, optional HTTP headers and connection timeout control, see [all configuration keys](https://github.com/clj-commons/aleph/blob/d33c76d6c7d1bf9788369fe6fd9d0e56434c8244/src/aleph/http.clj#L122-L132)
 
diff --git a/deps.edn b/deps.edn
index 826a7ef..24fbf0c 100644
--- a/deps.edn
+++ b/deps.edn
@@ -1,7 +1,7 @@
 ;; DO NOT EDIT MANUALLY - generated from project.clj via deps/lein-to-deps
 {:paths ["src" "resources" "target/classes"],
  :deps
- {org.clojure/clojure {:mvn/version "1.11.3"},
+ {org.clojure/clojure {:mvn/version "1.12.1"},
   org.clojure/tools.logging
   {:mvn/version "1.3.0", :exclusions [org.clojure/clojure]},
   manifold/manifold
@@ -9,27 +9,27 @@
   org.clj-commons/byte-streams {:mvn/version "0.3.4"},
   org.clj-commons/dirigiste {:mvn/version "1.0.4"},
   org.clj-commons/primitive-math {:mvn/version "1.0.1"},
-  potemkin/potemkin {:mvn/version "0.4.7"},
-  io.netty/netty-transport {:mvn/version "4.1.118.Final"},
+  potemkin/potemkin {:mvn/version "0.4.8"},
+  io.netty/netty-transport {:mvn/version "4.1.122.Final"},
   io.netty/netty-transport-native-epoll$linux-x86_64
-  {:mvn/version "4.1.118.Final"},
+  {:mvn/version "4.1.122.Final"},
   io.netty/netty-transport-native-epoll$linux-aarch_64
-  {:mvn/version "4.1.118.Final"},
+  {:mvn/version "4.1.122.Final"},
   io.netty/netty-transport-native-kqueue$osx-x86_64
-  {:mvn/version "4.1.118.Final"},
+  {:mvn/version "4.1.122.Final"},
   io.netty.incubator/netty-incubator-transport-native-io_uring$linux-x86_64
-  {:mvn/version "0.0.25.Final"},
+  {:mvn/version "0.0.26.Final"},
   io.netty.incubator/netty-incubator-transport-native-io_uring$linux-aarch_64
-  {:mvn/version "0.0.25.Final"},
-  io.netty/netty-codec {:mvn/version "4.1.118.Final"},
-  io.netty/netty-codec-http {:mvn/version "4.1.118.Final"},
-  io.netty/netty-codec-http2 {:mvn/version "4.1.118.Final"},
-  io.netty/netty-handler {:mvn/version "4.1.118.Final"},
-  io.netty/netty-handler-proxy {:mvn/version "4.1.118.Final"},
-  io.netty/netty-resolver {:mvn/version "4.1.118.Final"},
-  io.netty/netty-resolver-dns {:mvn/version "4.1.118.Final"},
+  {:mvn/version "0.0.26.Final"},
+  io.netty/netty-codec {:mvn/version "4.1.122.Final"},
+  io.netty/netty-codec-http {:mvn/version "4.1.122.Final"},
+  io.netty/netty-codec-http2 {:mvn/version "4.1.122.Final"},
+  io.netty/netty-handler {:mvn/version "4.1.122.Final"},
+  io.netty/netty-handler-proxy {:mvn/version "4.1.122.Final"},
+  io.netty/netty-resolver {:mvn/version "4.1.122.Final"},
+  io.netty/netty-resolver-dns {:mvn/version "4.1.122.Final"},
   metosin/malli
-  {:mvn/version "0.16.1", :exclusions [org.clojure/clojure]}},
+  {:mvn/version "0.19.1", :exclusions [org.clojure/clojure]}},
  :aliases
  {:lein2deps
   {:deps
diff --git a/examples/src/aleph/examples/http.clj b/examples/src/aleph/examples/http.clj
index e267c8d..d486f4b 100644
--- a/examples/src/aleph/examples/http.clj
+++ b/examples/src/aleph/examples/http.clj
@@ -46,7 +46,7 @@
    (hello-world-handler req)))
 
 (defn delayed-hello-world-handler
-  "Alternately, we can use a [core.async](https://github.com/clojure/core.async) goroutine to
+  "Alternatively, we can use a [core.async](https://github.com/clojure/core.async) goroutine to
    create our response, and convert the channel it returns using
    `manifold.deferred/->source`, and then take the first message from it. This is entirely equivalent
    to the previous implementation."
diff --git a/examples/src/aleph/examples/tcp.clj b/examples/src/aleph/examples/tcp.clj
index 5bee2b0..250fefe 100644
--- a/examples/src/aleph/examples/tcp.clj
+++ b/examples/src/aleph/examples/tcp.clj
@@ -149,7 +149,7 @@
              (s/put! s (str "ERROR: " ex))
              (s/close! s)))))))
 
-;; Alternately, we use `manifold.deferred/let-flow` to implement the composition of these
+;; Alternatively, we use `manifold.deferred/let-flow` to implement the composition of these
 ;; asynchronous values.  It is certainly more concise, but at the cost of being less explicit.
 (defn slow-echo-handler
   [f]
diff --git a/project.clj b/project.clj
index 71ffc75..ce0d0f9 100644
--- a/project.clj
+++ b/project.clj
@@ -1,25 +1,25 @@
 ;; you'll need to run the script at `deps/lein-to-deps` after changing any dependencies
-(def netty-version "4.1.118.Final")
-(def brotli-version "1.16.0")
+(def netty-version "4.1.122.Final")
+(def brotli-version "1.18.0")
 
 
 (defproject aleph (or (System/getenv "PROJECT_VERSION") "0.8.3")
   :description "A framework for asynchronous communication"
   :url "https://github.com/clj-commons/aleph"
   :license {:name "MIT License"}
-  :dependencies [[org.clojure/clojure "1.11.3"]
+  :dependencies [[org.clojure/clojure "1.12.1"]
                  [org.clojure/tools.logging "1.3.0" :exclusions [org.clojure/clojure]]
                  [manifold "0.4.3" :exclusions [org.clojure/tools.logging]]
                  [org.clj-commons/byte-streams "0.3.4"]
                  [org.clj-commons/dirigiste "1.0.4"]
                  [org.clj-commons/primitive-math "1.0.1"]
-                 [potemkin "0.4.7"]
+                 [potemkin "0.4.8"]
                  [io.netty/netty-transport ~netty-version]
                  [io.netty/netty-transport-native-epoll ~netty-version :classifier "linux-x86_64"]
                  [io.netty/netty-transport-native-epoll ~netty-version :classifier "linux-aarch_64"]
                  [io.netty/netty-transport-native-kqueue ~netty-version :classifier "osx-x86_64"]
-                 [io.netty.incubator/netty-incubator-transport-native-io_uring "0.0.25.Final" :classifier "linux-x86_64"]
-                 [io.netty.incubator/netty-incubator-transport-native-io_uring "0.0.25.Final" :classifier "linux-aarch_64"]
+                 [io.netty.incubator/netty-incubator-transport-native-io_uring "0.0.26.Final" :classifier "linux-x86_64"]
+                 [io.netty.incubator/netty-incubator-transport-native-io_uring "0.0.26.Final" :classifier "linux-aarch_64"]
                  [io.netty/netty-codec ~netty-version]
                  [io.netty/netty-codec-http ~netty-version]
                  [io.netty/netty-codec-http2 ~netty-version]
@@ -27,17 +27,17 @@
                  [io.netty/netty-handler-proxy ~netty-version]
                  [io.netty/netty-resolver ~netty-version]
                  [io.netty/netty-resolver-dns ~netty-version]
-                 [metosin/malli "0.16.1" :exclusions [org.clojure/clojure]]]
+                 [metosin/malli "0.19.1" :exclusions [org.clojure/clojure]]]
   :profiles {:dev                 {:dependencies [[criterium "0.4.6"]
-                                                  [cheshire "5.13.0"]
-                                                  [org.slf4j/slf4j-simple "2.0.13"]
+                                                  [cheshire "6.0.0"]
+                                                  [org.slf4j/slf4j-simple "2.0.17"]
                                                   [com.cognitect/transit-clj "1.0.333"]
                                                   [spootnik/signal "0.2.5"]
                                                   ;; This is for dev and testing ONLY, not recommended for prod
-                                                  [org.bouncycastle/bcprov-jdk18on "1.78.1"]
-                                                  [org.bouncycastle/bcpkix-jdk18on "1.78.1"]
+                                                  [org.bouncycastle/bcprov-jdk18on "1.81"]
+                                                  [org.bouncycastle/bcpkix-jdk18on "1.81"]
                                                   ;;[org.bouncycastle/bctls-jdk18on "1.75"]
-                                                  [io.netty/netty-tcnative-boringssl-static "2.0.65.Final"]
+                                                  [io.netty/netty-tcnative-boringssl-static "2.0.72.Final"]
                                                   ;;[com.aayushatharva.brotli4j/all ~brotli-version]
                                                   [com.aayushatharva.brotli4j/brotli4j ~brotli-version]
                                                   [com.aayushatharva.brotli4j/service ~brotli-version]
@@ -47,7 +47,7 @@
                                                   [com.aayushatharva.brotli4j/native-osx-aarch64 ~brotli-version]
                                                   [com.aayushatharva.brotli4j/native-osx-x86_64 ~brotli-version]
                                                   [com.aayushatharva.brotli4j/native-windows-x86_64 ~brotli-version]
-                                                  [com.github.luben/zstd-jni "1.5.6-3"]]
+                                                  [com.github.luben/zstd-jni "1.5.7-3"]]
                                    :jvm-opts     ["-Dorg.slf4j.simpleLogger.defaultLogLevel=debug"
                                                   "-Dorg.slf4j.simpleLogger.showThreadName=false"
                                                   "-Dorg.slf4j.simpleLogger.showThreadId=true"
@@ -66,7 +66,7 @@
                                          "-Dio.netty.allocator.type=unpooled"]}
              :pedantic            {:pedantic? :abort}
              :trace               {:jvm-opts ["-Dorg.slf4j.simpleLogger.defaultLogLevel=trace"]}
-             :profile             {:dependencies [[com.clojure-goes-fast/clj-async-profiler "1.2.2"]]
+             :profile             {:dependencies [[com.clojure-goes-fast/clj-async-profiler "1.6.2"]]
                                    :jvm-opts     ["-Djdk.attach.allowAttachSelf"]}}
   :java-source-paths ["src-java"]
   :test-selectors {:default   #(not
diff --git a/src/aleph/http.clj b/src/aleph/http.clj
index 08efc6a..a0a7d37 100644
--- a/src/aleph/http.clj
+++ b/src/aleph/http.clj
@@ -51,10 +51,11 @@
    | ---                               | --- |
    | `port`                            | The port the server will bind to. If `0`, the server will bind to a random port. |
    | `socket-address`                  | A `java.net.SocketAddress` specifying both the port and interface to bind to. |
+   | `existing-channel`                | A pre-bound `java.nio.channels.ServerSocketChannel` for the server to use rather than opening and binding its own server-socket. It won't be closed by the server. Possibly obtained from `System/inheritedChannel`. |
    | `bootstrap-transform`             | A function that takes an `io.netty.bootstrap.ServerBootstrap` object, which represents the server, and modifies it. |
    | `http-versions`                   | An optional vector of allowable HTTP versions to negotiate via ALPN, in preference order. Defaults to `[:http1]`. |
    | `ssl-context`                     | An `io.netty.handler.ssl.SslContext` object or a map of SSL context options (see `aleph.netty/ssl-server-context` for more details) if an SSL connection is desired. When passing an `io.netty.handler.ssl.SslContext` object, it must have an ALPN config matching the `http-versions` option (see `aleph.netty/ssl-server-context` and `aleph.netty/application-protocol-config`). If only HTTP/1.1 is desired, ALPN config is optional.
-   | `manual-ssl?`                     | Set to `true` to indicate that SSL is active, but the caller is managing it (this implies `:ssl-context` is nil). For example, this can be used if you want to use configure SNI (perhaps in `:pipeline-transform` ) to select the SSL context based on the client's indicated host name. |
+   | `manual-ssl?`                     | Set to `true` to indicate that SSL is active, but the caller is managing it (this implies `:ssl-context` is nil). For example, this can be used if you want to use configure SNI (usually via `:initial-pipeline-transform`) to select the SSL context based on the client's indicated host name. |
    | `executor`                        | A `java.util.concurrent.Executor` which is used to handle individual requests. To avoid this indirection you may specify `:none`, but in this case extreme care must be taken to avoid blocking operations on the handler's thread. |
    | `shutdown-executor?`              | If `true`, the executor will be shut down when `.close()` is called on the server, defaults to `true` . |
    | `request-buffer-size`             | The maximum body size, in bytes, that the server will allow to accumulate before placing on the body stream, defaults to `16384` . This does *not* represent the maximum size request the server can handle (which is unbounded), and is only a means of maximizing performance. |
@@ -69,6 +70,7 @@
    | `continue-handler`                | Optional handler which is invoked when header sends \"Except: 100-continue\" header to test whether the request should be accepted or rejected. Handler should return `true`, `false`, ring responseo to be used as a reject response or deferred that yields one of those. |
    | `continue-executor`               | Optional `java.util.concurrent.Executor` which is used to handle requests passed to :continue-handler. To avoid this indirection you may specify `:none`, but in this case extreme care must be taken to avoid blocking operations on the handler's thread. |
    | `shutdown-timeout`                | Interval in seconds within which in-flight requests must be processed, defaults to 15 seconds. A value of `0` bypasses waiting entirely. |
+   | `initial-pipeline-transform`      | A function that takes an `io.netty.channel.ChannelPipeline` object, which represents the connection before any default handlers are installed (and thus, before HTTP version negotiation), and modifies it. |
 
    HTTP/1-specific options
    | Param key                         | Description |
@@ -286,7 +288,7 @@
    | `ssl-context`         | an `io.netty.handler.ssl.SslContext` object, only required if a custom context is required
    | `ssl-endpoint-id-alg` | the name of the algorithm to use for SSL endpoint identification (see https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#endpoint-identification-algorithms), defaults to \"HTTPS\". Only used for WSS connections. Pass `nil` to disable endpoint identification.
    | `extensions?`         | if `true`, the websocket extensions will be supported.
-   | `sub-protocols`       | a string with a comma seperated list of supported sub-protocols.
+   | `sub-protocols`       | a string with a comma separated list of supported sub-protocols.
    | `headers`             | the headers that should be included in the handshake
    | `compression?`        | when set to `true`, enables client to use permessage-deflate compression extension, defaults to `false`.
    | `pipeline-transform`  | an optional function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.
@@ -310,7 +312,7 @@
    | ---                  | ---
    | `raw-stream?`        | if `true`, the connection will emit raw `io.netty.buffer.ByteBuf` objects rather than strings or byte-arrays.  This will minimize copying, but means that care must be taken with Netty's buffer reference counting.  Only recommended for advanced users.
    | `headers`            | the headers that should be included in the handshake
-   | `compression?`       | when set to `true`, enables permessage-deflate compression extention support for the connection, defaults to `false`.
+   | `compression?`       | when set to `true`, enables permessage-deflate compression extension support for the connection, defaults to `false`.
    | `pipeline-transform` | an optional function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.
    | `max-frame-payload`  | maximum allowable frame payload length, in bytes, defaults to `65536`.
    | `max-frame-size`     | maximum aggregate message size, in bytes, defaults to `1048576`.
@@ -324,7 +326,7 @@
 (defn websocket-ping
   "Takes a websocket endpoint (either client or server) and returns a deferred that will
    yield true whenever the PONG comes back, or false if the connection is closed. Subsequent
-   PINGs are supressed to avoid ambiguity in a way that the next PONG trigger all pending PINGs."
+   PINGs are suppressed to avoid ambiguity in a way that the next PONG trigger all pending PINGs."
   ([conn]
    (ws.common/websocket-ping conn (d/deferred) nil))
   ([conn d']
diff --git a/src/aleph/http/client.clj b/src/aleph/http/client.clj
index 019b0de..67f2beb 100644
--- a/src/aleph/http/client.clj
+++ b/src/aleph/http/client.clj
@@ -97,7 +97,7 @@
 (defn exception-handler [ctx ex response-stream]
   (log/warn "exception-handler" ex)
   (cond
-    ;; could happens when io.netty.handler.codec.http.HttpObjectAggregator
+    ;; could happen when io.netty.handler.codec.http.HttpObjectAggregator
     ;; is part of the pipeline
     (instance? TooLongFrameException ex)
     (s/put! response-stream ex)
@@ -336,7 +336,7 @@
                    (true? ssl?)))
       (throw (IllegalArgumentException.
                (str "Proxy options given require sending CONNECT request, "
-                    "but `tunnel?' option is set to 'false' explicitely. "
+                    "but `tunnel?' option is set to 'false' explicitly. "
                     "Consider setting 'tunnel?' to 'true' or omit it at all"))))
 
     (if (non-tunnel-proxy? options')
diff --git a/src/aleph/http/server.clj b/src/aleph/http/server.clj
index 1a9677d..34cd6c8 100644
--- a/src/aleph/http/server.clj
+++ b/src/aleph/http/server.clj
@@ -46,7 +46,8 @@
       LastHttpContent)
     (io.netty.handler.ssl
       ApplicationProtocolNames
-      SslContext)
+      SslContext
+      SslHandler)
     (io.netty.handler.stream
       ChunkedWriteHandler)
     (io.netty.util AsciiString)
@@ -633,7 +634,12 @@
 
 (defn make-pipeline-builder
   "Returns a function that initializes a new server channel's pipeline."
-  [handler {:keys [ssl? ^SslContext ssl-context use-h2c?] :as opts}]
+  [handler {:keys [ssl?
+                   ^SslContext ssl-context
+                   use-h2c?
+                   initial-pipeline-transform]
+            :or {initial-pipeline-transform identity}
+            :as opts}]
   (fn pipeline-builder*
     [^ChannelPipeline pipeline]
     (log/trace "pipeline-builder*" pipeline opts)
@@ -641,32 +647,35 @@
                             :handler handler
                             :server? true
                             :pipeline pipeline)]
+      (initial-pipeline-transform pipeline)
       (cond ssl?
-            (let [ssl-handler (netty/ssl-handler (.channel pipeline) ssl-context)]
-              (log/debug "Setting up secure HTTP server pipeline.")
-              (log/debug "ALPN HTTP versions:" (mapv str (.nextProtocols ssl-context)))
-
-              (-> pipeline
-                  (.addLast "ssl-handler" ssl-handler)
-                  (.addLast "apn-handler"
-                            (ApnHandler.
-                              (fn setup-secure-pipeline
-                                [^ChannelPipeline pipeline protocol]
-                                (log/trace "setup-secure-pipeline: chosen protocol:" protocol)
-                                (when (nil? (.applicationProtocol ssl-handler))
-                                  (log/debug (str "ALPN not used. Protocol " protocol " chosen by fallback.")))
-                                (cond (.equals ApplicationProtocolNames/HTTP_1_1 protocol)
-                                      (setup-http1-pipeline setup-opts)
-
-                                      (.equals ApplicationProtocolNames/HTTP_2 protocol)
-                                      (http2/setup-conn-pipeline setup-opts)
-
-                                      :else
-                                      (let [msg (str "Unknown protocol: " protocol)
-                                            e (IllegalStateException. msg)]
-                                        (log/error e msg)
-                                        (throw e))))
-                              apn-fallback-protocol)))
+            (do
+              ;; might be nil in manual-ssl? mode
+              (when ssl-context
+                (log/debug "Setting up secure HTTP server pipeline.")
+                (log/debug "ALPN HTTP versions:" (mapv str (.nextProtocols ssl-context)))
+                (.addLast pipeline "ssl-handler" (netty/ssl-handler (.channel pipeline) ssl-context)))
+              (.addLast pipeline
+                        "apn-handler"
+                        (ApnHandler.
+                         (fn setup-secure-pipeline
+                           [^ChannelPipeline pipeline protocol]
+                           (log/trace "setup-secure-pipeline: chosen protocol:" protocol)
+                           (let [^SslHandler ssl-handler (.get pipeline SslHandler)]
+                             (when (nil? (.applicationProtocol ssl-handler))
+                               (log/debug (str "ALPN not used. Protocol " protocol " chosen by fallback.")))
+                             (cond (.equals ApplicationProtocolNames/HTTP_1_1 protocol)
+                                   (setup-http1-pipeline setup-opts)
+
+                                   (.equals ApplicationProtocolNames/HTTP_2 protocol)
+                                   (http2/setup-conn-pipeline setup-opts)
+
+                                   :else
+                                   (let [msg (str "Unknown protocol: " protocol)
+                                         e (IllegalStateException. msg)]
+                                     (log/error e msg)
+                                     (throw e)))))
+                         apn-fallback-protocol))
               pipeline)
 
             use-h2c?
@@ -728,6 +737,7 @@
   [handler
    {:keys [port
            socket-address
+           existing-channel
            executor
            http-versions
            ssl-context
@@ -750,19 +760,13 @@
         opts (assoc opts :ssl-context ssl-context)
         http1-pipeline-transform (common/validate-http1-pipeline-transform opts)
         executor (setup-executor executor)
-        continue-executor (setup-continue-executor executor continue-executor)
-        pipeline-builder (make-pipeline-builder
-                           handler
-                           (assoc opts
-                                  :executor executor
-                                  :ssl? (or manual-ssl? (boolean ssl-context))
-                                  :http1-pipeline-transform http1-pipeline-transform
-                                  :continue-executor continue-executor))]
+        continue-executor (setup-continue-executor executor continue-executor)]
 
     (if (some #{:http2} http-versions)
       (when (and (not ssl-context)
-                 (not use-h2c?))
-        (throw (IllegalArgumentException. "HTTP/2 requires ssl-context to be given or use-h2c? to be true.")))
+                 (not use-h2c?)
+                 (not manual-ssl?))
+        (throw (IllegalArgumentException. "HTTP/2 requires passing an ssl-context or manual-ssl? true. Alternatively, pass use-h2c? true to disable TLS.")))
       (when use-h2c?
         (throw (IllegalArgumentException. "use-h2c? may only be true when HTTP/2 is enabled."))))
 
@@ -770,12 +774,23 @@
                use-h2c?)
       (throw (IllegalArgumentException. "use-h2c? must not be true when ssl-context is given.")))
 
+    (when (and ssl-context
+               manual-ssl?)
+      (throw (IllegalArgumentException. "manual-ssl? must not be true when ssl-context is given.")))
+
     (netty/start-server
-      {:pipeline-builder    pipeline-builder
+      {:pipeline-builder    (make-pipeline-builder
+                             handler
+                             (assoc opts
+                                    :executor executor
+                                    :ssl? (or manual-ssl? (boolean ssl-context))
+                                    :http1-pipeline-transform http1-pipeline-transform
+                                    :continue-executor continue-executor))
        :bootstrap-transform bootstrap-transform
-       :socket-address      (if socket-address
-                              socket-address
-                              (InetSocketAddress. port))
+       :socket-address      (cond
+                              socket-address socket-address
+                              (nil? existing-channel) (InetSocketAddress. port))
+       :existing-channel    existing-channel
        :on-close            (when (and shutdown-executor?
                                        (or (instance? ExecutorService executor)
                                            (instance? ExecutorService continue-executor)))
diff --git a/src/aleph/http/websocket/client.clj b/src/aleph/http/websocket/client.clj
index e1805c9..f3c59cd 100644
--- a/src/aleph/http/websocket/client.clj
+++ b/src/aleph/http/websocket/client.clj
@@ -190,7 +190,7 @@
              (if raw-stream?
                (let [body (.content ^TextWebSocketFrame msg)]
                  ;; pass ByteBuf body directly to lower next
-                 ;; level. it's their reponsibility to release
+                 ;; level. it's their responsibility to release
                  (netty/put! ch @in body))
                (let [text (.text ^TextWebSocketFrame msg)]
                  (netty/release msg)
diff --git a/src/aleph/http/websocket/server.clj b/src/aleph/http/websocket/server.clj
index c3e9f19..9300b50 100644
--- a/src/aleph/http/websocket/server.clj
+++ b/src/aleph/http/websocket/server.clj
@@ -112,7 +112,7 @@
                (if raw-stream?
                  (let [body (.content ^TextWebSocketFrame msg)]
                    ;; pass ByteBuf body directly to next level (it's
-                   ;; their reponsibility to release)
+                   ;; their responsibility to release)
                    (netty/put! ch in body))
                  (let [text (.text ^TextWebSocketFrame msg)]
                    ;; working with text now, so we do not need
diff --git a/src/aleph/netty.clj b/src/aleph/netty.clj
index 49336df..de2228b 100644
--- a/src/aleph/netty.clj
+++ b/src/aleph/netty.clj
@@ -21,6 +21,7 @@
       Unpooled)
     (io.netty.channel
       Channel
+      ChannelFactory
       ChannelFuture
       ChannelHandler
       ChannelHandlerContext
@@ -1193,7 +1194,7 @@
     (instance? SslContext ssl-context)
     ssl-context
 
-    ;; in future this option might be interesing
+    ;; in future this option might be interesting
     ;; for turning application config (e.g. ALPN)
     ;; depending on the server's capabilities
     (instance? SslContextBuilder ssl-context)
@@ -1667,6 +1668,22 @@
     (.addFirst pipeline "channel-tracker" ^ChannelHandler (channel-tracking-handler chan-group))
     (pipeline-builder pipeline)))
 
+(defn- validate-existing-channel
+  [existing-channel]
+  (when (some? existing-channel)
+    (when-not (instance? java.nio.channels.ServerSocketChannel existing-channel)
+      (throw (IllegalArgumentException.
+               (str "The existing-channel type is not supported: " (pr-str existing-channel)))))
+    (when (nil? (.getLocalAddress ^java.nio.channels.ServerSocketChannel existing-channel))
+      (throw (IllegalArgumentException.
+               (str "The existing-channel is not bound: " (pr-str existing-channel)))))))
+
+(defn- wrapping-channel-factory
+  ^ChannelFactory [^java.nio.channels.ServerSocketChannel channel]
+  (proxy [ChannelFactory] []
+    (newChannel []
+      (NioServerSocketChannel. channel))))
+
 (defn ^:no-doc start-server
   ([pipeline-builder
     ssl-context
@@ -1685,11 +1702,13 @@
             bootstrap-transform
             on-close
             ^SocketAddress socket-address
+            existing-channel
             transport
             shutdown-timeout]
      :or   {shutdown-timeout default-shutdown-timeout}
      :as   opts}]
    (ensure-transport-available! transport)
+   (validate-existing-channel existing-channel)
    (let [num-cores (.availableProcessors (Runtime/getRuntime))
          num-threads (* 2 num-cores)
          thread-factory (enumerating-thread-factory "aleph-server-pool" false)
@@ -1715,7 +1734,8 @@
                      (.option ChannelOption/SO_REUSEADDR true)
                      (.option ChannelOption/MAX_MESSAGES_PER_READ Integer/MAX_VALUE)
                      (.group group)
-                     (.channel channel-class)
+                     (cond-> (nil? existing-channel) (.channel channel-class))
+                     (cond-> (some? existing-channel) (.channelFactory (wrapping-channel-factory existing-channel)))
                      ;;TODO: add a server (.handler) call to the bootstrap, for logging or something
                      (.childHandler (pipeline-initializer pipeline-builder))
                      (.childOption ChannelOption/SO_REUSEADDR true)
@@ -1723,7 +1743,11 @@
                      bootstrap-transform)
 
              ^ServerSocketChannel
-             ch (-> b (.bind socket-address) .sync .channel)]
+             ch (-> (if (nil? existing-channel)
+                      (.bind b socket-address)
+                      (.register b))
+                    .sync
+                    .channel)]
 
          (reify
            Closeable
@@ -1731,7 +1755,9 @@
              (when (compare-and-set! closed? false true)
                ;; This is the three step closing sequence:
                ;; 1. Stop listening to incoming requests
-               (-> ch .close .sync)
+               (if (nil? existing-channel)
+                 (-> ch .close .sync)
+                 (-> ch .deregister .sync))
                (-> (if (pos? shutdown-timeout)
                      ;; 2. Wait for in-flight requests to stop processing within the supplied timeout
                      ;;    interval.
@@ -1759,7 +1785,8 @@
            (port [_]
              (-> ch .localAddress .getPort))
            (wait-for-close [_]
-             (-> ch .closeFuture .await)
+             (when (nil? existing-channel)
+               (-> ch .closeFuture .await))
              (-> group .terminationFuture .await)
              nil)))
 
diff --git a/src/aleph/tcp.clj b/src/aleph/tcp.clj
index c7d6b99..5c06437 100644
--- a/src/aleph/tcp.clj
+++ b/src/aleph/tcp.clj
@@ -74,6 +74,7 @@
    | ---                   | ---
    | `port`                | the port the server will bind to.  If `0`, the server will bind to a random port.
    | `socket-address`      | a `java.net.SocketAddress` specifying both the port and interface to bind to.
+   | `existing-channel`    | a pre-bound `java.nio.channels.ServerSocketChannel` for the server to use rather than opening and binding its own server-socket. It won't be closed by the server. Possibly obtained from `System/inheritedChannel`. |
    | `ssl-context`         | an `io.netty.handler.ssl.SslContext` object or a map of SSL context options (see `aleph.netty/ssl-server-context` for more details). If given, the server will only accept SSL connections and call the handler once the SSL session has been successfully established. If a self-signed certificate is all that's required, `(aleph.netty/self-signed-ssl-context)` will suffice.
    | `bootstrap-transform` | a function that takes an `io.netty.bootstrap.ServerBootstrap` object, which represents the server, and modifies it.
    | `pipeline-transform`  | a function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.
@@ -81,7 +82,7 @@
    | `shutdown-timeout`    | interval in seconds within which in-flight requests must be processed, defaults to 15 seconds. A value of 0 bypasses waiting entirely.
    | `transport`           | the transport to use, one of `:nio`, `:epoll`, `:kqueue` or `:io-uring` (defaults to `:nio`)."
   [handler
-   {:keys [port socket-address ssl-context bootstrap-transform pipeline-transform epoll?
+   {:keys [port socket-address existing-channel ssl-context bootstrap-transform pipeline-transform epoll?
            shutdown-timeout transport]
     :or {bootstrap-transform identity
          pipeline-transform identity
@@ -101,9 +102,10 @@
                                     (server-channel-handler handler options))
                           (pipeline-transform pipeline))
       :bootstrap-transform bootstrap-transform
-      :socket-address (if socket-address
-                        socket-address
-                        (InetSocketAddress. port))
+      :socket-address (cond
+                        socket-address socket-address
+                        (nil? existing-channel) (InetSocketAddress. port))
+      :existing-channel existing-channel
       :transport (netty/determine-transport transport epoll?)
       :shutdown-timeout shutdown-timeout})))
 


Test patch:
diff --git a/test/aleph/http_test.clj b/test/aleph/http_test.clj
index f04b6d2..a3b8154 100644
--- a/test/aleph/http_test.clj
+++ b/test/aleph/http_test.clj
@@ -2,12 +2,13 @@
   (:require
     [aleph.flow :as flow]
     [aleph.http :as http]
+    [aleph.http.common :as common]
     [aleph.http.core :as http.core]
     [aleph.netty :as netty]
     [aleph.resource-leak-detector]
     [aleph.ssl :as test-ssl]
     [aleph.tcp :as tcp]
-    [aleph.testutils :refer [str=]]
+    [aleph.testutils :refer [str= bound-channel]]
     [clj-commons.byte-streams :as bs]
     [clojure.java.io :as io]
     [clojure.string :as str]
@@ -47,7 +48,10 @@
     (java.io
       Closeable
       File)
-    (java.net UnknownHostException)
+    (java.net
+      BindException
+      UnknownHostException)
+    (java.nio.channels ServerSocketChannel)
     (java.util.concurrent
       SynchronousQueue
       ThreadPoolExecutor
@@ -518,6 +522,29 @@
         (is (= 200 (:status @(http-get "/"))))
         (is (some? @ssl-session))))))
 
+
+(defn test-manual-ssl-for-version [http-version]
+  (testing (str "manual ssl with " http-version)
+    (with-redefs [*use-tls-requests* true]
+      (with-server (http/start-server string-handler
+                                      (assoc http-server-options
+                                             :manual-ssl? true
+                                             :http-versions [http-version]
+                                             :initial-pipeline-transform (fn [pipeline]
+                                                                           (let [ssl-handler (netty/ssl-handler (.channel pipeline)
+                                                                                                                (-> test-ssl/server-ssl-context-opts
+                                                                                                                    (common/ensure-consistent-alpn-config [http-version])
+                                                                                                                    (netty/coerce-ssl-server-context)))]
+                                                                             (.addLast pipeline
+                                                                                       "ssl-handler"
+                                                                                       ssl-handler)))))
+        (binding [*connection-options* {:http-versions [http-version]}]
+          (is (= 200 (:status @(http-get "/")))))))))
+
+(deftest test-manual-ssl
+  (test-manual-ssl-for-version :http1)
+  (test-manual-ssl-for-version :http2))
+
 (deftest test-invalid-body
   (let [client-url "/"]
     (with-handler echo-handler
@@ -1274,7 +1301,7 @@
         (is (= "Internal Server Error" (bs/to-string (:body resp)))))))
 
   (testing "reading invalid request message: bad request"
-    ;; this request should fail with `IllegalArgumentException` "multiple Content-Lenght headers"
+    ;; this request should fail with `IllegalArgumentException` "multiple Content-Length headers"
     (with-tcp-request {} ["GET / HTTP/1.1"
                           "content-length: 0"
                           "content-length: 1"]
@@ -1449,7 +1476,7 @@
     (let [result (try-start-server
                   {:http-versions [:http2]})]
       (is (instance? IllegalArgumentException result))
-      (is (= "HTTP/2 requires ssl-context to be given or use-h2c? to be true." (ex-message result)))))
+      (is (= "HTTP/2 requires passing an ssl-context or manual-ssl? true. Alternatively, pass use-h2c? true to disable TLS." (ex-message result)))))
 
   (testing "HTTP/2 without ssl-context but with h2c"
     (let [result (try-start-server
@@ -1747,6 +1774,40 @@
             (catch Exception e
               (is (instance? ProxyConnectException e)))))))))
 
+(deftest test-existing-channel
+  (testing "validation"
+    (is (thrown-with-msg? Exception #"existing-channel"
+                          (with-http-servers basic-handler {:existing-channel "a string"})))
+    (with-open [unbound-channel (ServerSocketChannel/open)]
+      (is (thrown-with-msg? Exception #"existing-channel"
+                            (with-http-servers basic-handler {:existing-channel unbound-channel})))))
+
+  (testing "with a bound server-socket channel"
+    (testing "- unknown to the server"
+      (with-open [_ (bound-channel port)]
+        ;; The port is already bound by a server-socket channel, but
+        ;; we are not telling Aleph about it, so we should get a
+        ;; BindException when Aleph tries to bind to the same port.
+        (is (thrown? BindException
+                     (with-http-servers basic-handler {})))))
+
+    (testing "- known to the server"
+      (with-open [channel (bound-channel port)]
+        ;; This time, we shouldn't get a BindException, because we are
+        ;; telling Aleph to use an existing server-socket channel,
+        ;; which should be already bound, so Aleph doesn't try to bind.
+        (with-http-servers basic-handler {:existing-channel channel}
+          (is (= 200 (:status @(http-get "/string")))))
+        ;; The existing channel should not be closed on a server shutdown,
+        ;; because that channel is not owned by the server.
+        (is (.isOpen channel)))))
+
+  (testing "the :port option is not required when :existing-channel is passed"
+    (with-redefs [http-server-options (dissoc http-server-options :port)]
+      (with-open [channel (bound-channel port)]
+        (with-http1-server basic-handler {:existing-channel channel}
+          (is (= 200 (:status @(http-get "/string")))))))))
+
 (deftest ^:leak test-leak-in-raw-stream-handler
   ;; NOTE: Expecting 2 leaks because `with-raw-handler` will run its body for both http1 and
   ;; http2. It would be nicer to put this assertion into the body but the http1 server seems to
diff --git a/test/aleph/tcp_test.clj b/test/aleph/tcp_test.clj
index 11e2688..a871d16 100644
--- a/test/aleph/tcp_test.clj
+++ b/test/aleph/tcp_test.clj
@@ -3,6 +3,7 @@
    [aleph.netty :as netty]
    [aleph.resource-leak-detector]
    [aleph.tcp :as tcp]
+   [aleph.testutils :refer [bound-channel]]
    [clj-commons.byte-streams :as bs]
    [clojure.test :refer [deftest testing is]]
    [manifold.stream :as s]))
@@ -55,4 +56,13 @@
       (catch Exception _
         (is (not (netty/io-uring-available?)))))))
 
+(deftest test-existing-channel
+  (testing "the :port option is not required when :existing-channel is passed"
+    (let [port 8083]
+      (with-open [channel (bound-channel port)]
+        (with-server (tcp/start-server echo-handler {:existing-channel channel :shutdown-timeout 0})
+          (let [c @(tcp/client {:host "localhost" :port port})]
+            (s/put! c "foo")
+            (is (= "foo" (bs/to-string @(s/take! c))))))))))
+
 (aleph.resource-leak-detector/instrument-tests!)
diff --git a/test/aleph/testutils.clj b/test/aleph/testutils.clj
index f225c2c..06e8b69 100644
--- a/test/aleph/testutils.clj
+++ b/test/aleph/testutils.clj
@@ -1,8 +1,15 @@
 (ns aleph.testutils
-  (:import (io.netty.util AsciiString)))
+  (:import (io.netty.util AsciiString)
+           (java.net InetSocketAddress)
+           (java.nio.channels ServerSocketChannel)))
 
 (defn str=
   "AsciiString-aware equals"
   [^CharSequence x ^CharSequence y]
   (AsciiString/contentEquals x y))
 
+(defn bound-channel
+  "Returns a new server-socket channel bound to a `port`."
+  ^ServerSocketChannel [port]
+  (doto (ServerSocketChannel/open)
+    (.bind (InetSocketAddress. port))))