Skip to content

Commit

Permalink
feat(ai-proxy): disable HTTP/2 ALPN handshake for connections on rout…
Browse files Browse the repository at this point in the history
…es configured with AI-proxy. (#13735)

This change will disable HTTP/2 ALPN handshake for connections on routes configured with AI-proxy.

The following are the specific changes

- move tls related function kong/tls/plugins/certificate.lua and kong/tls/plugins/sni_filter.lua from ee to ce repo
- Based on feat(patch): add tls.disable_http2_alpn() function needed patch for disabling HTTP/2 ALPN when tls handshake. #13709 and feat: introduce tls.disable_http2_alpn() function lua-kong-nginx-module#93, we introduce the disable_http2_alpn action in the ai-proxy plugin to solve the ai-proxy plugin did not work in HTTP2 case. After the current PR is merged, HTTP/2 ALPN handshakes will be disabled for requests on routes configured with AI-proxy, and all these connections will fall back to the http1.1 protocol.

AG-119
  • Loading branch information
oowl authored Nov 4, 2024
1 parent 0072d7b commit 7d71b6b
Show file tree
Hide file tree
Showing 14 changed files with 608 additions and 33 deletions.
4 changes: 4 additions & 0 deletions changelog/unreleased/kong/feat-ai-proxy-disable-h2-alpn.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
message: |
**ai-proxy**: Disabled HTTP/2 ALPN handshake for connections on routes configured with AI-proxy.
type: feature
scope: Plugin
3 changes: 3 additions & 0 deletions kong-3.9.0-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ build = {
["kong.status"] = "kong/status/init.lua",
["kong.status.ready"] = "kong/status/ready.lua",

["kong.tls.plugins.certificate"] = "kong/tls/plugins/certificate.lua",
["kong.tls.plugins.sni_filter"] = "kong/tls/plugins/sni_filter.lua",

["kong.tools.dns"] = "kong/tools/dns.lua",
["kong.tools.grpc"] = "kong/tools/grpc.lua",
["kong.tools.utils"] = "kong/tools/utils.lua",
Expand Down
4 changes: 4 additions & 0 deletions kong/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,10 @@ function Kong.ssl_certificate()
kong.table.clear(ngx.ctx)
end

function Kong.ssl_client_hello()
local ctx = get_ctx_table(fetch_table(CTX_NS, CTX_NARR, CTX_NREC))
ctx.KONG_PHASE = PHASES.client_hello
end

function Kong.preread()
local ctx = get_ctx_table(fetch_table(CTX_NS, CTX_NARR, CTX_NREC))
Expand Down
73 changes: 73 additions & 0 deletions kong/llm/proxy/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ local kong_utils = require("kong.tools.gzip")
local buffer = require "string.buffer"
local strip = require("kong.tools.string").strip
local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy
local kong_global = require("kong.global")
local PHASES = kong_global.phases

local certificate = require("kong.tls.plugins.certificate")
local sni_filter = require("kong.tls.plugins.sni_filter")

local TTL_FOREVER = { ttl = 0 }
-- local SNI_CACHE_KEY = "ai:llm:cert_enabled_snis"

local EMPTY = require("kong.tools.table").EMPTY

Expand Down Expand Up @@ -477,4 +484,70 @@ function _M:access(conf)

end



function _M:build_http2_alpn_filter(plugin_name)
-- do not execute if the kong configuration doesn't have any http2 listeners
local http2_enabled = false
for _, listener in ipairs(kong.configuration.proxy_listeners) do
if listener.http2 then
http2_enabled = true
break
end
end

if not http2_enabled then
ngx.log(ngx.INFO, "no http2 listeners found, skipping LLM plugin initialization")
return
end

local sni_cache_key = "ai:llm:cert_enabled_snis:" .. plugin_name
local orig_ssl_client_hello = Kong.ssl_client_hello -- luacheck: ignore
Kong.ssl_client_hello = function() -- luacheck: ignore
orig_ssl_client_hello()

local ctx = ngx.ctx
-- ensure phases are set
ctx.KONG_PHASE = PHASES.client_hello

kong_global.set_namespaced_log(kong, plugin_name)
local snis_set, err = kong.cache:get(sni_cache_key, TTL_FOREVER,
sni_filter.build_ssl_route_filter_set, plugin_name)

if err then
kong.log.err("unable to request client to present its certificate: ",
err)
return ngx.exit(ngx.ERROR)
end
certificate.execute_client_hello(snis_set, { disable_http2 = true })
kong_global.reset_log(kong)

end

local worker_events = kong.worker_events
if not worker_events or not worker_events.register then
return
end

local function invalidate_sni_cache()
kong.cache:invalidate(sni_cache_key)
end

worker_events.register(function(data)
invalidate_sni_cache()
end, "crud", "plugins")

worker_events.register(function(data)
invalidate_sni_cache()
end, "crud", "routes")

worker_events.register(function(data)
invalidate_sni_cache()
end, "crud", "services")

worker_events.register(function(data)
invalidate_sni_cache()
end, "crud", "ca_certificates")
end

return _M
18 changes: 18 additions & 0 deletions kong/pdk/client/tls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,24 @@ local function new()
ngx.ctx.CLIENT_VERIFY_OVERRIDE = v
end

---
-- Prevents the TLS handshake from negotiating HTTP/2 ALPN.
-- if successful, the TLS handshake will not negotiate HTTP/2 ALPN to turn to HTTP1.1.
--
-- @function kong.client.tls.disable_http2_alpn
-- @phases client_hello
-- @treturn true|nil Returns `true` if successful, `nil` if it fails.
-- @treturn nil|err Returns `nil` if successful, or an error message if it fails.
--
-- @usage
-- local res, err = kong.client.tls.disable_http2_alpn()
-- if not res then
-- -- do something with err
-- end
function _TLS.disable_http2_alpn()
check_phase(PHASES.client_hello)
return kong_tls.disable_http2_alpn()
end

return _TLS
end
Expand Down
1 change: 1 addition & 0 deletions kong/pdk/private/phases.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ local PHASES = {
--init = 0x00000001,
init_worker = 0x00000001,
certificate = 0x00000002,
client_hello = 0x00000008,
--set = 0x00000004,
rewrite = 0x00000010,
access = 0x00000020,
Expand Down
4 changes: 3 additions & 1 deletion kong/plugins/ai-proxy/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ local deep_copy = require "kong.tools.table".deep_copy


local _M = deep_copy(require("kong.llm.proxy.handler"))

_M.init_worker = function()
_M:build_http2_alpn_filter("ai-proxy")
end

_M.PRIORITY = 770
_M.VERSION = kong_meta.version
Expand Down
3 changes: 3 additions & 0 deletions kong/templates/nginx_kong.lua
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ server {
ssl_certificate_by_lua_block {
Kong.ssl_certificate()
}
ssl_client_hello_by_lua_block {
Kong.ssl_client_hello()
}
> end
# injected nginx_proxy_* directives
Expand Down
3 changes: 3 additions & 0 deletions kong/templates/nginx_kong_stream.lua
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ server {
ssl_certificate_by_lua_block {
Kong.ssl_certificate()
}
ssl_client_hello_by_lua_block {
Kong.ssl_client_hello()
}
> end
set $upstream_host '';
Expand Down
128 changes: 128 additions & 0 deletions kong/tls/plugins/certificate.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
-- This software is copyright Kong Inc. and its licensors.
-- Use of the software is subject to the agreement between your organization
-- and Kong Inc. If there is no such agreement, use is governed by and
-- subject to the terms of the Kong Master Software License Agreement found
-- at https://konghq.com/enterprisesoftwarelicense/.
-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ]

--- Copyright 2019 Kong Inc.
local ngx_ssl = require "ngx.ssl"
local ssl_clt = require "ngx.ssl.clienthello"
local sni_filter = require("kong.tls.plugins.sni_filter")
local pl_stringx = require "pl.stringx"
local server_name = ngx_ssl.server_name
local PREFIX_SNIS_PSEUDO_INDEX = sni_filter.PREFIX_SNIS_PSEUDO_INDEX
local POSTFIX_SNIS_PSEUDO_INDEX = sni_filter.POSTFIX_SNIS_PSEUDO_INDEX
local startswith = pl_stringx.startswith
local endswith = pl_stringx.endswith

local _M = {}

local kong = kong
local EMPTY_T = {}


local function match_sni(snis, server_name)
if server_name then
-- search plain snis
if snis[server_name] then
kong.log.debug("matched the plain sni ", server_name)
return snis[server_name]
end

-- TODO: use radix tree to accelerate the search once we have an available implementation
-- search snis with the leftmost wildcard
for sni, sni_t in pairs(snis[POSTFIX_SNIS_PSEUDO_INDEX] or EMPTY_T) do
if endswith(server_name, sni_t.value) then
kong.log.debug(server_name, " matched the sni with the leftmost wildcard ", sni)
return sni_t
end
end

-- search snis with the rightmost wildcard
for sni, sni_t in pairs(snis[PREFIX_SNIS_PSEUDO_INDEX] or EMPTY_T) do
if startswith(server_name, sni_t.value) then
kong.log.debug(server_name, " matched the sni with the rightmost wildcard ", sni)
return sni_t
end
end
end

if server_name then
kong.log.debug("client sent an unknown sni ", server_name)

else
kong.log.debug("client didn't send an sni")
end

if snis["*"] then
kong.log.debug("mTLS is enabled globally")
return snis["*"]
end
end

function _M.execute(snis_set)

local server_name = server_name()

local sni_mapping = match_sni(snis_set, server_name)

if sni_mapping then
-- TODO: improve detection of ennoblement once we have DAO functions
-- to filter plugin configurations based on plugin name

kong.log.debug("enabled, will request certificate from client")

local chain
-- send CA DN list
if sni_mapping.ca_cert_chain then
kong.log.debug("set client ca certificate chain")
chain = sni_mapping.ca_cert_chain.ctx
end

local res, err = kong.client.tls.request_client_certificate(chain)
if not res then
kong.log.err("unable to request client to present its certificate: ",
err)
end

-- disable session resumption to prevent inability to access client
-- certificate in later phases
res, err = kong.client.tls.disable_session_reuse()
if not res then
kong.log.err("unable to disable session reuse for client certificate: ",
err)
end
end
end

function _M.execute_client_hello(snis_set, options)
if not snis_set then
return
end

if not options then
return
end

if not options.disable_http2 then
return
end

local server_name, err = ssl_clt.get_client_hello_server_name()
if err then
kong.log.debug("unable to get client hello server name: ", err)
return
end

local sni_mapping = match_sni(snis_set, server_name)

if sni_mapping then
local res, err = kong.client.tls.disable_http2_alpn()
if not res then
kong.log.err("unable to disable http2 alpn: ", err)
end
end
end

return _M
Loading

1 comment on commit 7d71b6b

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Bazel Build

Docker image available kong/kong:7d71b6b83601b541f6335cd06c09cc8e3d169138
Artifacts available https://github.com/Kong/kong/actions/runs/11659351398

Please sign in to comment.