Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: disable HTTP/2 ALPN handshake for connections on routes configured with AI-proxy. #13735

Merged
merged 12 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1051,6 +1051,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)
windmgc marked this conversation as resolved.
Show resolved Hide resolved

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

oowl marked this conversation as resolved.
Show resolved Hide resolved
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
Loading