From 4c7bf8cae4c1c0ccf1e40f651f9843e1d69fb300 Mon Sep 17 00:00:00 2001 From: Serris Lew Date: Mon, 17 Nov 2025 17:02:39 -0800 Subject: [PATCH 1/6] init remap vhost config --- configs/virtualhost.yaml.default | 13 + .../mgmt/rpc/handlers/config/Configuration.h | 1 + include/proxy/VirtualHost.h | 100 +++++ include/proxy/http/HttpSM.h | 5 +- include/proxy/http/remap/RemapConfig.h | 9 +- include/proxy/http/remap/UrlRewrite.h | 4 +- include/tscore/Filenames.h | 1 + src/mgmt/config/AddConfigFilesHere.cc | 1 + src/mgmt/rpc/handlers/config/Configuration.cc | 31 ++ src/proxy/CMakeLists.txt | 1 + src/proxy/ReverseProxy.cc | 3 + src/proxy/VirtualHost.cc | 347 ++++++++++++++++++ src/proxy/http/HttpSM.cc | 53 +++ src/proxy/http/remap/RemapConfig.cc | 44 ++- src/proxy/http/remap/UrlRewrite.cc | 18 +- src/records/RecordsConfig.cc | 2 + src/traffic_ctl/CtrlCommands.cc | 7 +- src/traffic_ctl/CtrlPrinters.cc | 7 +- src/traffic_ctl/jsonrpc/CtrlRPCRequests.h | 15 + src/traffic_ctl/traffic_ctl.cc | 3 +- src/traffic_server/RpcAdminPubHandlers.cc | 2 + 21 files changed, 642 insertions(+), 25 deletions(-) create mode 100644 configs/virtualhost.yaml.default create mode 100644 include/proxy/VirtualHost.h create mode 100644 src/proxy/VirtualHost.cc diff --git a/configs/virtualhost.yaml.default b/configs/virtualhost.yaml.default new file mode 100644 index 00000000000..06c6b2c741b --- /dev/null +++ b/configs/virtualhost.yaml.default @@ -0,0 +1,13 @@ +# virtualhost.yaml +# +# This configuration file defines a virtual host that provides domain-scoped configs and +# remap rules, overriding global configs. +# +# Example: +# virtualhost: +# - id: example +# domains: +# - example.com +# +# remap: +# - map http://example.com http://origin.example.com/ diff --git a/include/mgmt/rpc/handlers/config/Configuration.h b/include/mgmt/rpc/handlers/config/Configuration.h index e6f00b5aeaa..b7fa6fd08d4 100644 --- a/include/mgmt/rpc/handlers/config/Configuration.h +++ b/include/mgmt/rpc/handlers/config/Configuration.h @@ -26,5 +26,6 @@ namespace rpc::handlers::config { swoc::Rv set_config_records(std::string_view const &id, YAML::Node const ¶ms); swoc::Rv reload_config(std::string_view const &id, YAML::Node const ¶ms); +swoc::Rv reload_virtualhost_config(std::string_view const &id, YAML::Node const ¶ms); } // namespace rpc::handlers::config diff --git a/include/proxy/VirtualHost.h b/include/proxy/VirtualHost.h new file mode 100644 index 00000000000..ca6ab5af321 --- /dev/null +++ b/include/proxy/VirtualHost.h @@ -0,0 +1,100 @@ +/** @file + + Virtual Host configuration + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#pragma once + +#include +#include + +#include "iocore/eventsystem/ConfigProcessor.h" +#include "proxy/http/remap/UrlRewrite.h" +#include "tscore/Ptr.h" + +class VirtualHostConfig : public ConfigInfo +{ +public: + VirtualHostConfig() = default; + VirtualHostConfig(const VirtualHostConfig &other) : _entries(other._entries), _domains_to_id(other._domains_to_id) {} + VirtualHostConfig & + operator=(const VirtualHostConfig &other) + { + if (this != &other) { + _entries = other._entries; + _domains_to_id = other._domains_to_id; + } + return *this; + } + ~VirtualHostConfig() = default; + + struct Entry : public RefCountObjInHeap { + std::string id; + std::vector exact_domains; + std::vector regex_domains; + Ptr remap_table; + + Entry *acquire() const; + void release() const; + std::string get_id() const; + }; + + bool load(); + bool set_entry(Ptr &entry); + static bool load_entry(std::string_view id, Ptr &entry); + Ptr find_by_id(std::string_view id) const; + Ptr find_by_domain(std::string_view domain) const; + +private: + using entry_map = std::unordered_map>; + using name_map = std::unordered_map; + + entry_map _entries; + name_map _domains_to_id; +}; + +class VirtualHost +{ +public: + using scoped_config = ConfigProcessor::scoped_config; + + static void startup(); + static int reconfigure(); + static int reconfigure(std::string const &id); + static VirtualHostConfig *acquire(); + static void release(VirtualHostConfig *config); + +private: + static int config_callback(const char *, RecDataT, RecData, void *); + static int _configid; +}; + +struct VirtualHostConfigContinuation : public Continuation { + VirtualHostConfigContinuation() : Continuation(nullptr) { SET_HANDLER(&VirtualHostConfigContinuation::reconfigure); } + + int + reconfigure(int /* event ATS_UNUSED */, Event * /* e ATS_UNUSED */) + { + VirtualHost::reconfigure(); + delete this; + return EVENT_DONE; + } +}; diff --git a/include/proxy/http/HttpSM.h b/include/proxy/http/HttpSM.h index 3bbbd802a14..0e669a98597 100644 --- a/include/proxy/http/HttpSM.h +++ b/include/proxy/http/HttpSM.h @@ -45,6 +45,7 @@ #include "api/InkAPIInternal.h" #include "proxy/ProxyTransaction.h" #include "proxy/hdrs/HdrUtils.h" +#include "proxy/VirtualHost.h" // inknet #include "proxy/http/PreWarmManager.h" @@ -306,7 +307,8 @@ class HttpSM : public Continuation, public PluginUserArgs // This unfortunately can't go into the t_state, because of circular dependencies. We could perhaps refactor // this, with a lot of work, but this is easier for now. - UrlRewrite *m_remap = nullptr; + UrlRewrite *m_remap = nullptr; + VirtualHostConfig::Entry *m_virtualhost_entry = nullptr; History history; NetVConnection * @@ -364,6 +366,7 @@ class HttpSM : public Continuation, public PluginUserArgs // Y! ebalsa: remap handlers int state_remap_request(int event, void *data); + void set_virtualhost_entry(std::string_view domain); void do_remap_request(bool); // Cache Handlers diff --git a/include/proxy/http/remap/RemapConfig.h b/include/proxy/http/remap/RemapConfig.h index 8456dd846c3..eab3e219c66 100644 --- a/include/proxy/http/remap/RemapConfig.h +++ b/include/proxy/http/remap/RemapConfig.h @@ -25,6 +25,11 @@ #include "proxy/http/remap/AclFiltering.h" +namespace YAML +{ +class Node; +} + class UrlRewrite; #define BUILD_TABLE_MAX_ARGS 2048 @@ -79,7 +84,7 @@ struct BUILD_TABLE_INFO { }; const char *remap_parse_directive(BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize); -bool remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti); +bool remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti, YAML::Node const *remap_node = nullptr); const char *remap_validate_filter_args(acl_filter_rule **rule_pp, const char *const *argv, int argc, char *errStrBuf, size_t errStrBufSize, ACLBehaviorPolicy behavior_policy); @@ -87,7 +92,7 @@ const char *remap_validate_filter_args(acl_filter_rule **rule_pp, const char *co unsigned long remap_check_option(const char *const *argv, int argc, unsigned long findmode = 0, int *_ret_idx = nullptr, const char **argptr = nullptr); -bool remap_parse_config(const char *path, UrlRewrite *rewrite); +bool remap_parse_config(const char *path, UrlRewrite *rewrite, YAML::Node const *remap_node); using load_remap_file_func = void (*)(const char *, const char *); diff --git a/include/proxy/http/remap/UrlRewrite.h b/include/proxy/http/remap/UrlRewrite.h index dcc767ebe44..e335f9ba315 100644 --- a/include/proxy/http/remap/UrlRewrite.h +++ b/include/proxy/http/remap/UrlRewrite.h @@ -79,12 +79,14 @@ class UrlRewrite : public RefCountObjInHeap */ bool load(); + bool load_table(const std::string &config_file_path, YAML::Node const *remap_node); + /** Build the internal url write tables. * * @param path Path to configuration file. * @return 0 on success, non-zero error code on failure. */ - int BuildTable(const char *path); + int BuildTable(const char *path, YAML::Node const *remap_node = nullptr); mapping_type Remap_redirect(HTTPHdr *request_header, URL *redirect_url); bool ReverseMap(HTTPHdr *response_header); diff --git a/include/tscore/Filenames.h b/include/tscore/Filenames.h index 90ae0485df9..e6e4c928523 100644 --- a/include/tscore/Filenames.h +++ b/include/tscore/Filenames.h @@ -43,6 +43,7 @@ namespace filename constexpr const char *SPLITDNS = "splitdns.config"; constexpr const char *SNI = "sni.yaml"; constexpr const char *JSONRPC = "jsonrpc.yaml"; + constexpr const char *VIRTUALHOST = "virtualhost.yaml"; /////////////////////////////////////////////////////////////////// // Various other file names diff --git a/src/mgmt/config/AddConfigFilesHere.cc b/src/mgmt/config/AddConfigFilesHere.cc index 38ff8abad96..bb9ffce6a69 100644 --- a/src/mgmt/config/AddConfigFilesHere.cc +++ b/src/mgmt/config/AddConfigFilesHere.cc @@ -78,4 +78,5 @@ initializeRegistry() registerFile("proxy.config.ssl.server.multicert.filename", ts::filename::SSL_MULTICERT, NOT_REQUIRED); registerFile("proxy.config.ssl.servername.filename", ts::filename::SNI, NOT_REQUIRED); registerFile("proxy.config.jsonrpc.filename", ts::filename::JSONRPC, NOT_REQUIRED); + registerFile("proxy.config.virtualhost.filename", ts::filename::VIRTUALHOST, NOT_REQUIRED); } diff --git a/src/mgmt/rpc/handlers/config/Configuration.cc b/src/mgmt/rpc/handlers/config/Configuration.cc index cf3f2fc60d5..7ae81091d46 100644 --- a/src/mgmt/rpc/handlers/config/Configuration.cc +++ b/src/mgmt/rpc/handlers/config/Configuration.cc @@ -30,6 +30,7 @@ #include "../common/RecordsUtils.h" #include "tsutil/Metrics.h" +#include "proxy/VirtualHost.h" namespace utils = rpc::handlers::records::utils; @@ -206,4 +207,34 @@ reload_config(std::string_view const & /* id ATS_UNUSED */, YAML::Node const & / return resp; } + +swoc::Rv +reload_virtualhost_config(std::string_view const & /* id ATS_UNUSED */, YAML::Node const ¶ms) +{ + swoc::Rv resp; + auto node = params["virtualhost"]; + if (!node || !node.IsScalar()) { + resp.note("Failed to specify virtualhost"); + return resp; + } + + std::string name = node.as(); + if (!VirtualHost::reconfigure(name)) { + resp.note("Failed to reload virtualhost configuration"); + return resp; + } + + VirtualHost::scoped_config vhost_config; + auto vhost_entry = vhost_config->find_by_id(name); + if (vhost_entry) { + resp.result()["virtualhost"] = name; + resp.result()["status"] = "ok"; + resp.result()["message"] = "Virtualhost successfully reloaded"; + } else { + resp.result()["virtualhost"] = name; + resp.result()["status"] = "missing"; + resp.result()["message"] = "Virtualhost missing or removed after reload"; + } + return resp; +} } // namespace rpc::handlers::config diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index 93c16697845..69c78e43c04 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -36,6 +36,7 @@ add_library( Transform.cc FetchSM.cc PluginHttpConnect.cc + VirtualHost.cc ) add_library(ts::proxy ALIAS proxy) diff --git a/src/proxy/ReverseProxy.cc b/src/proxy/ReverseProxy.cc index 341c2c7de40..9f467a23eec 100644 --- a/src/proxy/ReverseProxy.cc +++ b/src/proxy/ReverseProxy.cc @@ -39,6 +39,7 @@ #include "proxy/http/remap/RemapProcessor.h" #include "proxy/http/remap/UrlRewrite.h" #include "proxy/http/remap/UrlMapping.h" +#include "proxy/VirtualHost.h" namespace { @@ -85,6 +86,8 @@ init_reverse_proxy() // Hold at least one lease, until we reload the configuration rewrite_table->acquire(); + VirtualHost::startup(); + return 0; } diff --git a/src/proxy/VirtualHost.cc b/src/proxy/VirtualHost.cc new file mode 100644 index 00000000000..7ade5e669f1 --- /dev/null +++ b/src/proxy/VirtualHost.cc @@ -0,0 +1,347 @@ +/** @file + + Virtual Host configuration implementation + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include "proxy/VirtualHost.h" +#include "records/RecCore.h" +#include "tscore/Filenames.h" + +namespace +{ +DbgCtl dbg_ctl_virtualhost("virtualhost"); +} + +int VirtualHost::_configid = 0; + +VirtualHostConfig::Entry * +VirtualHostConfig::Entry::acquire() const +{ + auto *self = const_cast(this); + if (self) { + self->refcount_inc(); + } + return self; +} + +void +VirtualHostConfig::Entry::release() const +{ + auto *self = const_cast(this); + if (self && self->refcount_dec() == 0) { + self->free(); + } +} + +std::string +VirtualHostConfig::Entry::get_id() const +{ + return id; +} + +std::set valid_vhost_keys = {"id", "domains", "remap"}; + +template <> struct YAML::convert { + static bool + decode(const YAML::Node &node, VirtualHostConfig::Entry &item) + { + for (const auto &elem : node) { + if (std::none_of(valid_vhost_keys.begin(), valid_vhost_keys.end(), + [&elem](const std::string &s) { return s == elem.first.as(); })) { + Warning("unsupported key '%s' in VirtualHost config", elem.first.as().c_str()); + } + } + + if (!node["id"]) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry must provide `id`"); + return false; + } + item.id = node["id"].as(); + + auto domains = node["domains"]; + if (!domains || !domains.IsSequence() || domains.size() == 0) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry must provide at least one domain in `domains` sequence"); + return false; + } + for (const auto &it : domains) { + // TODO: filter/normalize domain name + auto domain = it.as(); + if (domain.empty()) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry can't have empty domain entry"); + return false; + } + item.exact_domains.push_back(std::move(domain)); + // TODO: add regex domain check + } + return true; + } +}; + +bool +build_virtualhost_entry(YAML::Node const &node, Ptr &entry) +{ + entry.clear(); + Ptr vhost = make_ptr(new VirtualHostConfig::Entry); + auto &conf = *vhost; + try { + if (!YAML::convert::decode(node, conf)) { + return false; + } + } catch (YAML::Exception const &ex) { + Dbg(dbg_ctl_virtualhost, "Failed to parse virtualhost entry"); + return false; + } + + // Build UrlRewrite table for remap rules + auto remap_node = node["remap"]; + if (remap_node) { + auto table = std::make_unique(); + if (!table->load_table(conf.id, &remap_node)) { + Dbg(dbg_ctl_virtualhost, "Failed to load remap rules for virtualhost entry"); + return false; + } + conf.remap_table = make_ptr(table.release()); + } + entry = std::move(vhost); + return true; +} + +bool +VirtualHostConfig::load() +{ + _entries.clear(); + std::string config_path = RecConfigReadConfigPath("proxy.config.virtualhost.filename", ts::filename::VIRTUALHOST); + + try { + YAML::Node config = YAML::LoadFile(config_path); + if (config.IsNull()) { + Dbg(dbg_ctl_virtualhost, "Empty virtualhost config: %s", config_path.c_str()); + return true; + } + + config = config["virtualhost"]; + if (config.IsNull() || !config.IsSequence()) { + Dbg(dbg_ctl_virtualhost, "Expected toplevel 'virtualhost' key to be a sequence"); + return false; + } + + for (auto const &node : config) { + Ptr entry; + if (!build_virtualhost_entry(node, entry)) { + return false; + } + + std::string vhost_id = entry->id; + if (_entries.contains(vhost_id)) { + Dbg(dbg_ctl_virtualhost, "Duplicate virtualhost id: %s", vhost_id.c_str()); + return false; + } + + for (auto const &domain : entry->exact_domains) { + if (_domains_to_id.contains(domain)) { + Dbg(dbg_ctl_virtualhost, "Domain (%s) already in another virtualhost config", domain.c_str()); + return false; + } + _domains_to_id[domain] = vhost_id; + } + + _entries[vhost_id] = std::move(entry); + } + + } catch (std::exception &ex) { + Dbg(dbg_ctl_virtualhost, "Failed to load %s: %s", config_path.c_str(), ex.what()); + return false; + } + return true; +} + +bool +VirtualHostConfig::load_entry(std::string_view id, Ptr &entry) +{ + entry.clear(); + std::string config_path = RecConfigReadConfigPath("proxy.config.virtualhost.filename", ts::filename::VIRTUALHOST); + + try { + YAML::Node config = YAML::LoadFile(config_path); + if (config.IsNull()) { + Dbg(dbg_ctl_virtualhost, "Empty virtualhost config: %s", config_path.c_str()); + return true; + } + + config = config["virtualhost"]; + if (config.IsNull() || !config.IsSequence()) { + Dbg(dbg_ctl_virtualhost, "Expected toplevel 'virtualhost' key to be a sequence"); + return false; + } + + for (auto const &node : config) { + auto config_id = node["id"]; + if (!config_id || config_id.as() != id) { + continue; + } + + Ptr vhost_entry; + if (!build_virtualhost_entry(node, vhost_entry)) { + return false; + } + entry = std::move(vhost_entry); + return true; + } + + } catch (std::exception &ex) { + Dbg(dbg_ctl_virtualhost, "Failed to load virtualhost entry (%s) in %s: %s", id.data(), config_path.c_str(), ex.what()); + return false; + } + Dbg(dbg_ctl_virtualhost, "Virtualhost with id (%s) not found", id.data()); + return false; +} + +bool +VirtualHostConfig::set_entry(Ptr &entry) +{ + std::string vhost_id = entry->id; + auto it = _entries.find(vhost_id); + if (it != _entries.end()) { + Ptr curr_entry = std::move(it->second); + for (auto const &domain : curr_entry->exact_domains) { + _domains_to_id.erase(domain); + } + } + for (auto const &domain : entry->exact_domains) { + if (_domains_to_id.contains(domain)) { + Dbg(dbg_ctl_virtualhost, "Domain (%s) already in another virtualhost config", domain.c_str()); + return 0; + } + _domains_to_id[domain] = vhost_id; + } + _entries[vhost_id] = std::move(entry); + return true; +} + +Ptr +VirtualHostConfig::find_by_id(std::string_view id) const +{ + if (_entries.empty()) { + return Ptr(); + } + + auto entry = _entries.find(std::string{id}); + if (entry != _entries.end()) { + return entry->second; + } + return Ptr(); +} + +Ptr +VirtualHostConfig::find_by_domain(std::string_view domain) const +{ + if (_entries.empty() || _domains_to_id.empty() || domain.empty()) { + return Ptr(); + } + + auto id = _domains_to_id.find(std::string{domain}); + if (id != _domains_to_id.end()) { + auto entry = _entries.find(id->second); + if (entry != _entries.end()) { + return entry->second; + } + } + + // TODO: look through regex domains if exact domain is not found + + return Ptr(); +} + +void +VirtualHost::startup() +{ + reconfigure(); + RecRegisterConfigUpdateCb("proxy.config.virtualhost.filename", &VirtualHost::config_callback, nullptr); +} + +int +VirtualHost::reconfigure() +{ + Note("%s loading ...", ts::filename::VIRTUALHOST); + auto config = std::make_unique(); + + if (!config->load()) { + return 0; + } + + _configid = configProcessor.set(_configid, config.release()); + + Note("%s finished loading", ts::filename::VIRTUALHOST); + return 1; +} + +int +VirtualHost::reconfigure(std::string const &id) +{ + VirtualHost::scoped_config vhost_config; + Dbg(dbg_ctl_virtualhost, "Reconfiguring virtualhost entry: %s", id.c_str()); + // Reconfigure all vhosts if id not specified + if (id.empty()) { + Dbg(dbg_ctl_virtualhost, "No virtualhost specified, reconfiguring all entries"); + return reconfigure(); + } + + Ptr entry; + if (!VirtualHostConfig::load_entry(id, entry)) { + return 0; + } + + auto config = std::make_unique(*vhost_config); + + if (!config->set_entry(entry)) { + return 0; + } + + _configid = configProcessor.set(_configid, config.release()); + return 1; +} + +VirtualHostConfig * +VirtualHost::acquire() +{ + return static_cast(configProcessor.get(_configid)); +} + +void +VirtualHost::release(VirtualHostConfig *config) +{ + if (config && _configid > 0) { + configProcessor.release(_configid, config); + } +} + +int +VirtualHost::config_callback(const char *, RecDataT, RecData, void *) +{ + eventProcessor.schedule_imm(new VirtualHostConfigContinuation, ET_TASK); + return 0; +} diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index 8b2b4477b6d..5f606ea5637 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -253,6 +253,11 @@ HttpSM::HttpSM() : Continuation(nullptr), vc_table(this) {} HttpSM::~HttpSM() { + if (m_virtualhost_entry) { + m_virtualhost_entry->release(); + m_virtualhost_entry = nullptr; + } + http_parser_clear(&http_parser); HttpConfig::release(t_state.http_config_param); @@ -4418,13 +4423,61 @@ HttpSM::check_sni_host() } } +void +HttpSM::set_virtualhost_entry(std::string_view domain) +{ + VirtualHost::scoped_config vhost_config; + // If already set, don't need to look at configs + if (m_virtualhost_entry || domain.empty() || !vhost_config) { + return; + } + + auto vhost_entry = vhost_config->find_by_domain(std::string{domain}); + if (vhost_entry) { + SMDbg(dbg_ctl_url_rewrite, "Found virtualhost: %s", vhost_entry->get_id().c_str()); + // Explicitly acquire() since HttpSM holds raw pointer + m_virtualhost_entry = vhost_entry->acquire(); + } +} + void HttpSM::do_remap_request(bool run_inline) { SMDbg(dbg_ctl_http_seq, "Remapping request"); SMDbg(dbg_ctl_url_rewrite, "Starting a possible remapping for request"); + + if (!m_virtualhost_entry) { + auto host_name{t_state.hdr_info.client_request.host_get()}; + set_virtualhost_entry(host_name); + } + + // Check virtualhost remap rules before looking at remap.config + bool virtualhost_remap = false; + if (m_virtualhost_entry && m_virtualhost_entry->remap_table) { + UrlRewrite *vhost_table = m_virtualhost_entry->remap_table->acquire(); + if (vhost_table) { + // If already acquired, release ref + if (vhost_table == m_remap) { + vhost_table->release(); + } else { + m_remap->release(); + m_remap = vhost_table; + } + SMDbg(dbg_ctl_url_rewrite, "Using virtualhost remap table: %s", m_virtualhost_entry->get_id().c_str()); + virtualhost_remap = true; + } + } + bool ret = remapProcessor.setup_for_remap(&t_state, m_remap); + // If no remap matches in virtualhost, revert to default remap.config + if (!ret && virtualhost_remap) { + SMDbg(dbg_ctl_url_rewrite, "No virtualhost remap rules found: using global remap table"); + m_remap->release(); + m_remap = rewrite_table->acquire(); + ret = remapProcessor.setup_for_remap(&t_state, m_remap); + } + check_sni_host(); // Depending on a variety of factors the HOST field may or may not have been promoted to the diff --git a/src/proxy/http/remap/RemapConfig.cc b/src/proxy/http/remap/RemapConfig.cc index a1ceac4e0ee..49468e62616 100644 --- a/src/proxy/http/remap/RemapConfig.cc +++ b/src/proxy/http/remap/RemapConfig.cc @@ -21,6 +21,8 @@ * limitations under the License. */ +#include + #include "proxy/http/remap/AclFiltering.h" #include "swoc/swoc_file.h" @@ -1033,7 +1035,7 @@ process_regex_mapping_config(const char *from_host_lower, url_mapping *new_mappi } bool -remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti) +remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti, YAML::Node const *remap_node) { char errBuf[1024]; char errStrBuf[1024]; @@ -1066,14 +1068,34 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti) bool is_cur_mapping_regex; const char *type_id_str; - std::error_code ec; - std::string content{swoc::file::load(swoc::file::path{path}, ec)}; - if (ec.value() == ENOENT) { // a missing file is ok - treat as empty, no rules. - return true; - } - if (ec.value()) { - Warning("Failed to open remapping configuration file %s - %s", path, strerror(ec.value())); - return false; + std::string content; + if (remap_node) { + if (!remap_node->IsSequence()) { + Error("Remap node must be a sequence"); + return false; + } + + for (auto const &remap_rule : *remap_node) { + if (!remap_rule.IsScalar()) { + Error("Inline remap rule must be a string"); + return false; + } + + auto remap = remap_rule.as(); + content.append(remap); + content.push_back('\n'); + } + } else { + std::error_code ec; + content = swoc::file::load(swoc::file::path{path}, ec); + + if (ec.value() == ENOENT) { // a missing file is ok - treat as empty, no rules. + return true; + } + if (ec.value()) { + Warning("Failed to open remapping configuration file %s - %s", path, strerror(ec.value())); + return false; + } } Dbg(dbg_ctl_url_rewrite, "[BuildTable] UrlRewrite::BuildTable()"); @@ -1486,7 +1508,7 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti) } bool -remap_parse_config(const char *path, UrlRewrite *rewrite) +remap_parse_config(const char *path, UrlRewrite *rewrite, YAML::Node const *remap_node) { BUILD_TABLE_INFO bti; @@ -1495,7 +1517,7 @@ remap_parse_config(const char *path, UrlRewrite *rewrite) rewrite->pluginFactory.indicatePreReload(); bti.rewrite = rewrite; - bool status = remap_parse_config_bti(path, &bti); + bool status = remap_parse_config_bti(path, &bti, remap_node); /* Now after we parsed the configuration and (re)loaded plugins and plugin instances * accordingly notify all plugins that we are done */ diff --git a/src/proxy/http/remap/UrlRewrite.cc b/src/proxy/http/remap/UrlRewrite.cc index 6c7e3f48d6c..0faa3924df8 100644 --- a/src/proxy/http/remap/UrlRewrite.cc +++ b/src/proxy/http/remap/UrlRewrite.cc @@ -77,14 +77,18 @@ UrlRewrite::get_acl_behavior_policy(ACLBehaviorPolicy &policy) bool UrlRewrite::load() { - ats_scoped_str config_file_path; - - config_file_path = RecConfigReadConfigPath("proxy.config.url_remap.filename", ts::filename::REMAP); - if (!config_file_path) { + std::string config_file_path = RecConfigReadConfigPath("proxy.config.url_remap.filename", ts::filename::REMAP); + if (config_file_path.empty()) { Warning("%s Unable to locate %s. No remappings in effect", modulePrefix, ts::filename::REMAP); return false; } + return load_table(config_file_path, nullptr); +} + +bool +UrlRewrite::load_table(const std::string &config_file_path, YAML::Node const *remap_node) +{ this->ts_name = nullptr; if (auto rec_str{RecGetRecordStringAlloc("proxy.config.proxy_name")}; rec_str) { this->ts_name = ats_stringdup(rec_str); @@ -132,7 +136,7 @@ UrlRewrite::load() Dbg(dbg_ctl_url_rewrite_regex, "strategyFactory file: %s", sf.c_str()); strategyFactory = new NextHopStrategyFactory(sf.c_str()); - if (TS_SUCCESS == this->BuildTable(config_file_path)) { + if (TS_SUCCESS == this->BuildTable(config_file_path.c_str(), remap_node)) { int n_rules = this->rule_count(); // Minimum # of rules to be considered a valid configuration. int required_rules; required_rules = RecGetRecordInt("proxy.config.url_remap.min_rules_required").value_or(0); @@ -816,7 +820,7 @@ UrlRewrite::InsertForwardMapping(mapping_type maptype, url_mapping *mapping, con */ int -UrlRewrite::BuildTable(const char *path) +UrlRewrite::BuildTable(const char *path, YAML::Node const *remap_node) { ink_assert(forward_mappings.empty()); ink_assert(reverse_mappings.empty()); @@ -835,7 +839,7 @@ UrlRewrite::BuildTable(const char *path) temporary_redirects.hash_lookup.reset(new URLTable); forward_mappings_with_recv_port.hash_lookup.reset(new URLTable); - if (!remap_parse_config(path, this)) { + if (!remap_parse_config(path, this, remap_node)) { return TS_ERROR; } diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index 7ca85a7553a..c461832a817 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -1108,6 +1108,8 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.url_remap.acl_behavior_policy", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , + {RECT_CONFIG, "proxy.config.virtualhost.filename", RECD_STRING, ts::filename::VIRTUALHOST, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + , //############################################################################## //# diff --git a/src/traffic_ctl/CtrlCommands.cc b/src/traffic_ctl/CtrlCommands.cc index 6d7a386e276..2d3c8eddea9 100644 --- a/src/traffic_ctl/CtrlCommands.cc +++ b/src/traffic_ctl/CtrlCommands.cc @@ -240,7 +240,12 @@ ConfigCommand::config_set() void ConfigCommand::config_reload() { - _printer->write_output(invoke_rpc(ConfigReloadRequest{})); + auto vhost = get_parsed_arguments()->get("virtualhost"); + if (vhost && !vhost.value().empty()) { + _printer->write_output(invoke_rpc(ConfigReloadVirtualHostRequest{vhost.value()})); + } else { + _printer->write_output(invoke_rpc(ConfigReloadRequest{})); + } } void ConfigCommand::config_show_file_registry() diff --git a/src/traffic_ctl/CtrlPrinters.cc b/src/traffic_ctl/CtrlPrinters.cc index 55e51de20e6..61d0c6434c5 100644 --- a/src/traffic_ctl/CtrlPrinters.cc +++ b/src/traffic_ctl/CtrlPrinters.cc @@ -172,8 +172,13 @@ DiffConfigPrinter::write_output(YAML::Node const &result) } //------------------------------------------------------------------------------------------------------------------------------------ void -ConfigReloadPrinter::write_output([[maybe_unused]] YAML::Node const &result) +ConfigReloadPrinter::write_output(YAML::Node const &result) { + if (result.IsMap() && result["virtualhost"]) { + std::cout << "┌ Virtualhost: " << result["virtualhost"] << '\n'; + std::cout << "└┬ Reload status: " << result["status"] << '\n'; + std::cout << " ├ Message: " << result["message"] << '\n'; + } } //------------------------------------------------------------------------------------------------------------------------------------ void diff --git a/src/traffic_ctl/jsonrpc/CtrlRPCRequests.h b/src/traffic_ctl/jsonrpc/CtrlRPCRequests.h index acf1118873f..1407102ca15 100644 --- a/src/traffic_ctl/jsonrpc/CtrlRPCRequests.h +++ b/src/traffic_ctl/jsonrpc/CtrlRPCRequests.h @@ -48,6 +48,21 @@ struct ConfigReloadRequest : shared::rpc::ClientRequest { return "admin_config_reload"; } }; +struct ConfigReloadVirtualHostRequest : shared::rpc::ClientRequest { + using super = shared::rpc::ClientRequest; + ConfigReloadVirtualHostRequest() = default; + ConfigReloadVirtualHostRequest(std::string name) + { + if (!name.empty()) { + super::params["virtualhost"] = std::move(name); + } + } + std::string + get_method() const override + { + return "admin_config_reload_virtualhost"; + } +}; //------------------------------------------------------------------------------------------------------------------------------------ /// /// @brief To fetch config file registry from the RPC node. diff --git a/src/traffic_ctl/traffic_ctl.cc b/src/traffic_ctl/traffic_ctl.cc index 5599bd64c9a..1083f6e35ad 100644 --- a/src/traffic_ctl/traffic_ctl.cc +++ b/src/traffic_ctl/traffic_ctl.cc @@ -121,7 +121,8 @@ main([[maybe_unused]] int argc, const char **argv) .add_option("--records", "", "Emit output in YAML format") .add_option("--default", "", "Include the default value"); config_command.add_command("reload", "Request a configuration reload", Command_Execute) - .add_example_usage("traffic_ctl config reload"); + .add_example_usage("traffic_ctl config reload") + .add_option("--virtualhost", "", "Reload only the specific virtual host entry by id", "", 1, ""); config_command.add_command("status", "Check the configuration status", Command_Execute) .add_example_usage("traffic_ctl config status"); config_command.add_command("set", "Set a configuration value", "", 2, Command_Execute) diff --git a/src/traffic_server/RpcAdminPubHandlers.cc b/src/traffic_server/RpcAdminPubHandlers.cc index 4ab2706f444..4a47da50b8e 100644 --- a/src/traffic_server/RpcAdminPubHandlers.cc +++ b/src/traffic_server/RpcAdminPubHandlers.cc @@ -39,6 +39,8 @@ register_admin_jsonrpc_handlers() rpc::add_method_handler("admin_config_set_records", &set_config_records, &core_ats_rpc_service_provider_handle, {{rpc::RESTRICTED_API}}); rpc::add_method_handler("admin_config_reload", &reload_config, &core_ats_rpc_service_provider_handle, {{rpc::RESTRICTED_API}}); + rpc::add_method_handler("admin_config_reload_virtualhost", &reload_virtualhost_config, &core_ats_rpc_service_provider_handle, + {{rpc::RESTRICTED_API}}); // HostDB using namespace rpc::handlers::hostdb; From 927260ccc71e2b5a93c9c37af499b0fc4c997cd4 Mon Sep 17 00:00:00 2001 From: Serris Lew Date: Wed, 19 Nov 2025 14:55:54 -0800 Subject: [PATCH 2/6] add wildcard support --- configs/virtualhost.yaml.default | 10 +++ include/proxy/VirtualHost.h | 21 +++-- src/proxy/VirtualHost.cc | 132 +++++++++++++++++++++++-------- 3 files changed, 124 insertions(+), 39 deletions(-) diff --git a/configs/virtualhost.yaml.default b/configs/virtualhost.yaml.default index 06c6b2c741b..14d354f6f78 100644 --- a/configs/virtualhost.yaml.default +++ b/configs/virtualhost.yaml.default @@ -3,11 +3,21 @@ # This configuration file defines a virtual host that provides domain-scoped configs and # remap rules, overriding global configs. # +# Remap rule config flow: +# 1. Resolve to a single virtualhost +# A. Look through exact match virtualhost domains. If found, use virtualhost config. +# B. Look through wildcard virtualhost doamins. If found, use virtualhost config. +# C. If no virtualhost config found, skip to 3. +# 2. Within virtualhost config, use virtualhost remap rules. +# A. Follow existing remap.config rules. If found, use remap rule. (See remap.config for details) +# 3. If no virtualhost or remap rule found, use global remap.config +# # Example: # virtualhost: # - id: example # domains: # - example.com +# - "*.com" # Only allow single left-most: "*.[domain]" format # # remap: # - map http://example.com http://origin.example.com/ diff --git a/include/proxy/VirtualHost.h b/include/proxy/VirtualHost.h index ca6ab5af321..1b7d0bf31c3 100644 --- a/include/proxy/VirtualHost.h +++ b/include/proxy/VirtualHost.h @@ -34,13 +34,19 @@ class VirtualHostConfig : public ConfigInfo { public: VirtualHostConfig() = default; - VirtualHostConfig(const VirtualHostConfig &other) : _entries(other._entries), _domains_to_id(other._domains_to_id) {} + VirtualHostConfig(const VirtualHostConfig &other) + : _entries(other._entries), + _exact_domains_to_id(other._exact_domains_to_id), + _wildcard_domains_to_id(other._wildcard_domains_to_id) + { + } VirtualHostConfig & operator=(const VirtualHostConfig &other) { if (this != &other) { - _entries = other._entries; - _domains_to_id = other._domains_to_id; + _entries = other._entries; + _exact_domains_to_id = other._exact_domains_to_id; + _wildcard_domains_to_id = other._wildcard_domains_to_id; } return *this; } @@ -49,7 +55,7 @@ class VirtualHostConfig : public ConfigInfo struct Entry : public RefCountObjInHeap { std::string id; std::vector exact_domains; - std::vector regex_domains; + std::vector wildcard_domains; Ptr remap_table; Entry *acquire() const; @@ -58,7 +64,7 @@ class VirtualHostConfig : public ConfigInfo }; bool load(); - bool set_entry(Ptr &entry); + bool set_entry(std::string_view id, Ptr &entry); static bool load_entry(std::string_view id, Ptr &entry); Ptr find_by_id(std::string_view id) const; Ptr find_by_domain(std::string_view domain) const; @@ -68,7 +74,8 @@ class VirtualHostConfig : public ConfigInfo using name_map = std::unordered_map; entry_map _entries; - name_map _domains_to_id; + name_map _exact_domains_to_id; + name_map _wildcard_domains_to_id; }; class VirtualHost @@ -78,7 +85,7 @@ class VirtualHost static void startup(); static int reconfigure(); - static int reconfigure(std::string const &id); + static int reconfigure(std::string_view id); static VirtualHostConfig *acquire(); static void release(VirtualHostConfig *config); diff --git a/src/proxy/VirtualHost.cc b/src/proxy/VirtualHost.cc index 7ade5e669f1..52a86eb43f2 100644 --- a/src/proxy/VirtualHost.cc +++ b/src/proxy/VirtualHost.cc @@ -30,6 +30,7 @@ #include "proxy/VirtualHost.h" #include "records/RecCore.h" #include "tscore/Filenames.h" +#include "tsutil/Convert.h" namespace { @@ -87,16 +88,36 @@ template <> struct YAML::convert { Dbg(dbg_ctl_virtualhost, "Virtual host entry must provide at least one domain in `domains` sequence"); return false; } + item.exact_domains.clear(); + item.wildcard_domains.clear(); + for (const auto &it : domains) { - // TODO: filter/normalize domain name - auto domain = it.as(); - if (domain.empty()) { + auto domain_entry = it.as(); + if (domain_entry.empty()) { Dbg(dbg_ctl_virtualhost, "Virtual host entry can't have empty domain entry"); return false; } - item.exact_domains.push_back(std::move(domain)); - // TODO: add regex domain check + char domain[TS_MAX_HOST_NAME_LEN + 1]; + ts::transform_lower(domain_entry, domain); + + // Check if domain is wildcard, prefixed with * + if (domain[0] == '*') { + const char *subdomain = index(domain, '*'); + if (subdomain && subdomain[1] == '.') { + item.wildcard_domains.push_back(subdomain + 2); + } else { + Dbg(dbg_ctl_virtualhost, "Virtual host wildcard entry must have '*.[domain]' format"); + } + } else { + item.exact_domains.push_back(domain); + } } + + if (item.exact_domains.empty() && item.wildcard_domains.empty()) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry must have at least one domain defined"); + return false; + } + return true; } }; @@ -155,21 +176,29 @@ VirtualHostConfig::load() return false; } - std::string vhost_id = entry->id; + std::string vhost_id{entry->id}; if (_entries.contains(vhost_id)) { Dbg(dbg_ctl_virtualhost, "Duplicate virtualhost id: %s", vhost_id.c_str()); return false; } for (auto const &domain : entry->exact_domains) { - if (_domains_to_id.contains(domain)) { - Dbg(dbg_ctl_virtualhost, "Domain (%s) already in another virtualhost config", domain.c_str()); + if (_exact_domains_to_id.contains(domain)) { + Dbg(dbg_ctl_virtualhost, "Exact domain (%s) already in another virtualhost config", domain.c_str()); return false; } - _domains_to_id[domain] = vhost_id; + _exact_domains_to_id.emplace(domain, vhost_id); } - _entries[vhost_id] = std::move(entry); + for (auto const &domain_suffix : entry->wildcard_domains) { + if (_wildcard_domains_to_id.contains(domain_suffix)) { + Dbg(dbg_ctl_virtualhost, "Wildcard domain (%s) already in another virtualhost config", domain_suffix.c_str()); + return false; + } + _wildcard_domains_to_id.emplace(domain_suffix, vhost_id); + } + + _entries.emplace(vhost_id, std::move(entry)); } } catch (std::exception &ex) { @@ -217,28 +246,45 @@ VirtualHostConfig::load_entry(std::string_view id, Ptr &entry) return false; } Dbg(dbg_ctl_virtualhost, "Virtualhost with id (%s) not found", id.data()); - return false; + return true; } bool -VirtualHostConfig::set_entry(Ptr &entry) +VirtualHostConfig::set_entry(std::string_view id, Ptr &entry) { - std::string vhost_id = entry->id; - auto it = _entries.find(vhost_id); - if (it != _entries.end()) { + std::string vhost_id{id}; + // If virtualhost entry already exists, remove current entry + if (auto it = _entries.find(vhost_id); it != _entries.end()) { Ptr curr_entry = std::move(it->second); for (auto const &domain : curr_entry->exact_domains) { - _domains_to_id.erase(domain); + _exact_domains_to_id.erase(domain); + } + for (auto const &domain : curr_entry->wildcard_domains) { + _wildcard_domains_to_id.erase(domain); } + _entries.erase(vhost_id); } - for (auto const &domain : entry->exact_domains) { - if (_domains_to_id.contains(domain)) { - Dbg(dbg_ctl_virtualhost, "Domain (%s) already in another virtualhost config", domain.c_str()); - return 0; + + // Add new entry into virtualhost config + if (entry) { + for (auto const &domain : entry->exact_domains) { + if (_exact_domains_to_id.contains(domain)) { + Dbg(dbg_ctl_virtualhost, "Exact domain (%s) already in another virtualhost config", domain.c_str()); + return false; + } + _exact_domains_to_id.emplace(domain, vhost_id); } - _domains_to_id[domain] = vhost_id; + + for (auto const &domain_suffix : entry->wildcard_domains) { + if (_wildcard_domains_to_id.contains(domain_suffix)) { + Dbg(dbg_ctl_virtualhost, "Wildcard domain (%s) already in another virtualhost config", domain_suffix.c_str()); + return false; + } + _wildcard_domains_to_id.emplace(domain_suffix, vhost_id); + } + + _entries.emplace(vhost_id, std::move(entry)); } - _entries[vhost_id] = std::move(entry); return true; } @@ -259,19 +305,34 @@ VirtualHostConfig::find_by_id(std::string_view id) const Ptr VirtualHostConfig::find_by_domain(std::string_view domain) const { - if (_entries.empty() || _domains_to_id.empty() || domain.empty()) { + if (_entries.empty() || domain.empty()) { return Ptr(); } - auto id = _domains_to_id.find(std::string{domain}); - if (id != _domains_to_id.end()) { + char lower_domain[TS_MAX_HOST_NAME_LEN + 1]; + ts::transform_lower(std::string{domain}, lower_domain); + + // Check for exact match domains first + auto id = _exact_domains_to_id.find(lower_domain); + if (id != _exact_domains_to_id.end()) { auto entry = _entries.find(id->second); if (entry != _entries.end()) { return entry->second; } } - // TODO: look through regex domains if exact domain is not found + // Check wildcard suffixes + const char *subdomain = index(lower_domain, '.'); + while (subdomain) { + subdomain++; + if (auto suffix_id = _wildcard_domains_to_id.find(subdomain); suffix_id != _wildcard_domains_to_id.end()) { + auto entry = _entries.find(suffix_id->second); + if (entry != _entries.end()) { + return entry->second; + } + } + subdomain = index(subdomain, '.'); + } return Ptr(); } @@ -279,7 +340,9 @@ VirtualHostConfig::find_by_domain(std::string_view domain) const void VirtualHost::startup() { - reconfigure(); + if (!reconfigure()) { + Fatal("failed to load %s", ts::filename::VIRTUALHOST); + } RecRegisterConfigUpdateCb("proxy.config.virtualhost.filename", &VirtualHost::config_callback, nullptr); } @@ -290,6 +353,7 @@ VirtualHost::reconfigure() auto config = std::make_unique(); if (!config->load()) { + Error("%s failed to load", ts::filename::VIRTUALHOST); return 0; } @@ -300,10 +364,10 @@ VirtualHost::reconfigure() } int -VirtualHost::reconfigure(std::string const &id) +VirtualHost::reconfigure(std::string_view id) { VirtualHost::scoped_config vhost_config; - Dbg(dbg_ctl_virtualhost, "Reconfiguring virtualhost entry: %s", id.c_str()); + Dbg(dbg_ctl_virtualhost, "Reconfiguring virtualhost entry: %s", id.data()); // Reconfigure all vhosts if id not specified if (id.empty()) { Dbg(dbg_ctl_virtualhost, "No virtualhost specified, reconfiguring all entries"); @@ -315,12 +379,16 @@ VirtualHost::reconfigure(std::string const &id) return 0; } - auto config = std::make_unique(*vhost_config); + std::unique_ptr config; + if (vhost_config) { + config = std::make_unique(*vhost_config); + } else { + config = std::make_unique(); + } - if (!config->set_entry(entry)) { + if (!config->set_entry(id, entry)) { return 0; } - _configid = configProcessor.set(_configid, config.release()); return 1; } From 6c7801f55e96c2d5bc0afd71894c4ca0c0be5dd6 Mon Sep 17 00:00:00 2001 From: Serris Lew Date: Wed, 19 Nov 2025 15:39:55 -0800 Subject: [PATCH 3/6] allow missing vhost config --- src/proxy/VirtualHost.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/proxy/VirtualHost.cc b/src/proxy/VirtualHost.cc index 52a86eb43f2..a7f0a935c6b 100644 --- a/src/proxy/VirtualHost.cc +++ b/src/proxy/VirtualHost.cc @@ -157,6 +157,12 @@ VirtualHostConfig::load() _entries.clear(); std::string config_path = RecConfigReadConfigPath("proxy.config.virtualhost.filename", ts::filename::VIRTUALHOST); + struct stat sbuf; + if (stat(config_path.c_str(), &sbuf) == -1 && errno == ENOENT) { + Warning("Virtualhost configuration '%s' doesn't exist", config_path.c_str()); + return true; + } + try { YAML::Node config = YAML::LoadFile(config_path); if (config.IsNull()) { From 92a15c58e530a670ac2d6cea3f733a62a41886b7 Mon Sep 17 00:00:00 2001 From: Serris Lew Date: Fri, 21 Nov 2025 10:43:43 -0800 Subject: [PATCH 4/6] add docs --- doc/admin-guide/files/index.en.rst | 4 + doc/admin-guide/files/records.yaml.en.rst | 7 + doc/admin-guide/files/virtualhost.yaml.en.rst | 192 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 doc/admin-guide/files/virtualhost.yaml.en.rst diff --git a/doc/admin-guide/files/index.en.rst b/doc/admin-guide/files/index.en.rst index 1ce15d62421..5a3097cdc8f 100644 --- a/doc/admin-guide/files/index.en.rst +++ b/doc/admin-guide/files/index.en.rst @@ -38,6 +38,7 @@ Configuration Files sni.yaml.en storage.config.en strategies.yaml.en + virtualhost.yaml.en volume.config.en jsonrpc.yaml.en @@ -91,6 +92,9 @@ Configuration Files :doc:`jsonrpc.yaml.en` Defines some of the configurable arguments of the jsonrpc endpoint. +:doc:`virtualhost.yaml.en` + Defines configuration blocks that apply to a group of domains (virtualhosts). + .. note:: Currently the YAML parsing library has a bug where line number counting diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index 360cfc5fc0d..1becf68cd66 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -5697,3 +5697,10 @@ AIO ============ ====================================================================== Note: If you force the backend to use io_uring, you might experience failures with some (older, pre 5.4) kernel versions + +VirtualHost +=========== + +.. ts:cv:: CONFIG proxy.config.virtualhost.filename STRING virtualhost.yaml + + Sets the name of the :file:`virtualhost.yaml` file. diff --git a/doc/admin-guide/files/virtualhost.yaml.en.rst b/doc/admin-guide/files/virtualhost.yaml.en.rst new file mode 100644 index 00000000000..d4fbf1b6d42 --- /dev/null +++ b/doc/admin-guide/files/virtualhost.yaml.en.rst @@ -0,0 +1,192 @@ + +.. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. include:: ../../common.defs + +.. configfile:: virtualhost.yaml + +virtualhost.yaml +**************** + +The :file:`virtualhost.yaml` file defines configuration blocks that apply to a group of domains. +Each virtual host entry defines a set of domains and the remap rules associated with those domains. +Virtual host remap rules override global :file:`remap.config` rules but remain fully backward compatible +with existing configurations. If absent, ATS behaves exactly as before. + +Currently, this file only supports :file:`remap.config` overrides. Future versions will expand virtual +host support to additional configuration types (e.g. :file:`sni.yaml`, :file:`ssl_multicert.config`, +:file:`parent.config`, etc) + +By default this is named :file:`virtualhost.yaml`. The filename can be changed by setting +:ts:cv:`proxy.config.virtualhost.filename`. + + +Configuration +============= + +:file:`virtualhost.yaml` is YAML format with top level namespace **virtualhost** and a list of virtual host +entries. Each virtual host entry must provide an **id** and at least one domain defined in **domains**. + +An example configuration looks like: + +.. code-block:: yaml + + virtualhost: + - id: example + domains: + - example.com + + remap: + - map http://example.com http://origin.example.com/ + + +===================== ========================================================== +Field Name Description +===================== ========================================================== +``id`` Virtual host identifier to perform specific operations on +``domains`` List of domains to resolve a request to +``remap`` List of remap rules as defined in remap.config +===================== ========================================================== + +``domains`` + Domains can be defined as request domain name or subdomains using wildcard feature. + Wildcard support only allows single left most ``*``. This does not support regex. + When matching to a virtual host entry, domains with exact match have precedence + over wildcard. If a domain matches to multiple wildcard domains, the virtual host + config defined first has precedence. + + For example: + Supported: + - ``foo.example.com`` + - ``*.example.com`` + - ``*.com`` + + NOT Supported: + - ``foo[0-9]+.example.com`` (regex) + - ``bar.*.example.net`` (``*`` in the middle) + - ``*.bar.*.com`` (multiple ``*``) + - ``*.*.baz.com`` (multiple ``*``) + - ``baz*.example.net`` (partial wildcard) + - ``*baz.example.net`` (partial wildcard) + - ``b*z.example.net`` (partial wildcard) + - ``*`` (global) + +Evaluation Order +---------------- + +|TS| evaluates a request using deterministic precedence in the following order: + +1. Resolve to a single virtualhost + a. Check for an exact domain match. If any virtual host lists the request hostname explicitly, that virtual host is selected. + b. Check for a wildcard domain match. If any virtual host wildcard domains define a subdomain of the request hostname in the form ``*.[domain]``, that virtual host is selected. + c. If no matching virtual host exists, the request proceeds using global configuration (i.e :file:`remap.config`). Skip to step 3. +2. Within selected virtual host config, use virtual host remap rules. + a. Follow existing :file:`remap.config` rules and matching orders. If a matching remap rule is found, that remap rule is selected. +3. If neither virtual host nor remap rules match, ATS falls back to global :file:`remap.config` resolution. + +Only one virtual host entry may match a given request. If multiple entries could match, ATS uses the first matching +entry defined in :file:`virtualhost.yaml`. + + +Granular Reload +=============== + +|TS| now supports granular configuration reloads for individual virtual hosts defined in :file:`virtualhost.yaml`. +In addition to reloading the entire |TS| configuration with :option:`traffic_ctl config reload`, users can +selectively reload a single virtual host entry without affecting other virtual host entries. + +By only updating the necessary changes, this reduces configuration deployment time and improves visibility on the changes made. + +To reload for a specific virtual host, use: + +:: + + $ traffic_ctl config reload --virtualhost + +Where **** is the virtual host ID defined in :file:`virtualhost.yaml`. Only the **** virtual host +configuration will be reloaded. This does not affect other virtual hosts or global configuration files. + +Example: + +:: + + $ traffic_ctl config reload --virtualhost foo + ┌ Virtualhost: foo + └┬ Reload status: ok + ├ Message: Virtualhost successfully reloaded + + +Examples +======== + +.. code-block:: yaml + + # virtualhost.yaml + virtualhost: + - id: example + domains: + - example.com + + remap: + - map http://example.com/ http://origin.example.com/ + + # remap.config + map / http://other.example.com/ + +This rules translates in the following translation. + +================================================ ======================================================== +Client Request Translated Request +================================================ ======================================================== +``http://example.com/index.html`` ``http://origin.example.com/index.html`` +``http://www.x.com/index.html`` ``http://other.example.com/index.html`` +================================================ ======================================================== + +.. code-block:: yaml + + # virtualhost.yaml + virtualhost: + - id: example + domains: + - "*.example.com" + + remap: + - regex_map http://sub[0-9]+.example.com/ http://origin$1.example.com/ + + - id: foo + domains: + - foo.example.com + + remap: + - map http:/foo.example.com/ http://foo.origin.com/ + +This rules translates in the following translation. + +================================================ ======================================================== +Client Request Translated Request +================================================ ======================================================== +``http://sub0.example.com/index.html`` ``http://origin0.example.com/index.html`` +``http://foo.example.com/index.html`` ``http://foo.origin.com/index.html`` +``http://bar.example.com/index.html`` No remap rule found in virtual host entry `example` +================================================ ======================================================== + + +See Also +======== + +:file:`remap.config` From 50d775fe7def670e84a18202eae24f690afffc6a Mon Sep 17 00:00:00 2001 From: Serris Lew Date: Tue, 23 Dec 2025 10:45:02 -0800 Subject: [PATCH 5/6] add nullptr checks --- src/mgmt/rpc/handlers/config/Configuration.cc | 7 ++++++- src/proxy/http/HttpSM.cc | 10 +++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/mgmt/rpc/handlers/config/Configuration.cc b/src/mgmt/rpc/handlers/config/Configuration.cc index 7ae81091d46..89577ba940a 100644 --- a/src/mgmt/rpc/handlers/config/Configuration.cc +++ b/src/mgmt/rpc/handlers/config/Configuration.cc @@ -225,7 +225,12 @@ reload_virtualhost_config(std::string_view const & /* id ATS_UNUSED */, YAML::No } VirtualHost::scoped_config vhost_config; - auto vhost_entry = vhost_config->find_by_id(name); + if (!vhost_config) { + resp.note("Virtualhost not initialized"); + return resp; + } + + auto vhost_entry = vhost_config->find_by_id(name); if (vhost_entry) { resp.result()["virtualhost"] = name; resp.result()["status"] = "ok"; diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index 5f606ea5637..903c11e4595 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -4432,7 +4432,7 @@ HttpSM::set_virtualhost_entry(std::string_view domain) return; } - auto vhost_entry = vhost_config->find_by_domain(std::string{domain}); + auto vhost_entry = vhost_config->find_by_domain(domain); if (vhost_entry) { SMDbg(dbg_ctl_url_rewrite, "Found virtualhost: %s", vhost_entry->get_id().c_str()); // Explicitly acquire() since HttpSM holds raw pointer @@ -4460,7 +4460,9 @@ HttpSM::do_remap_request(bool run_inline) if (vhost_table == m_remap) { vhost_table->release(); } else { - m_remap->release(); + if (m_remap) { + m_remap->release(); + } m_remap = vhost_table; } SMDbg(dbg_ctl_url_rewrite, "Using virtualhost remap table: %s", m_virtualhost_entry->get_id().c_str()); @@ -4473,7 +4475,9 @@ HttpSM::do_remap_request(bool run_inline) // If no remap matches in virtualhost, revert to default remap.config if (!ret && virtualhost_remap) { SMDbg(dbg_ctl_url_rewrite, "No virtualhost remap rules found: using global remap table"); - m_remap->release(); + if (m_remap) { + m_remap->release(); + } m_remap = rewrite_table->acquire(); ret = remapProcessor.setup_for_remap(&t_state, m_remap); } From a68f97c4fc18b10a081e4cfd40f692eec45bf9a3 Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Tue, 23 Dec 2025 18:56:30 +0000 Subject: [PATCH 6/6] Add backtrace to crash logs Send crash information from the signal handler to traffic_crashlog. The backtrace is obtained by traffic_crashlog via ptrace rather than in the signal handler, since backtrace() can acquire locks that the crashing thread might be holding, causing a deadlock. This approach is safer because traffic_crashlog runs as a separate process and can use ptrace to inspect the crashed process without risking deadlock from lock contention. Add diagnostic output to the crash log when backtrace collection fails, so we can identify which step of the ptrace/unwind process is failing. --- include/tscore/ink_stack_trace.h | 17 +++ src/traffic_crashlog/backtrace.cc | 24 ++++- src/traffic_crashlog/traffic_crashlog.cc | 47 ++++++--- src/traffic_crashlog/traffic_crashlog.h | 19 ++-- src/traffic_server/Crash.cc | 10 ++ src/tscore/ink_stack_trace.cc | 125 +++++++++++++++++++++++ 6 files changed, 218 insertions(+), 24 deletions(-) diff --git a/include/tscore/ink_stack_trace.h b/include/tscore/ink_stack_trace.h index 4ec4e6d11e1..2c483f832eb 100644 --- a/include/tscore/ink_stack_trace.h +++ b/include/tscore/ink_stack_trace.h @@ -23,6 +23,8 @@ #pragma once +#include + // The max number of levels in the stack trace #define INK_STACK_TRACE_MAX_LEVELS 100 @@ -33,3 +35,18 @@ void ink_stack_trace_dump(); Get symbol of @n-th frame */ const void *ink_backtrace(const int n); + +/** + Get a demangled stack trace and write it to a file descriptor. + + @param[in] fd The file descriptor to write to. + @return The number of bytes written, or -1 on error. +*/ +ssize_t ink_stack_trace_dump_to_fd(int fd); + +/** + Get a demangled stack trace as a string. + + @param[out] bt The string to populate with the backtrace. +*/ +void ink_stack_trace_get(std::string &bt); diff --git a/src/traffic_crashlog/backtrace.cc b/src/traffic_crashlog/backtrace.cc index ab0ae983b3d..c82d41c1668 100644 --- a/src/traffic_crashlog/backtrace.cc +++ b/src/traffic_crashlog/backtrace.cc @@ -105,10 +105,12 @@ backtrace_for_thread(pid_t threadid, TextBuffer &text) void *ap = nullptr; pid_t target = -1; unsigned level = 0; + int step_result; // First, attach to the child, causing it to stop. status = ptrace(PTRACE_ATTACH, threadid, 0, 0); if (status < 0) { + text.format(" [ptrace ATTACH failed: %s (%d)]\n", strerror(errno), errno); Dbg(dbg_ctl_backtrace, "ptrace(ATTACH, %ld) -> %s (%d)\n", (long)threadid, strerror(errno), errno); return; } @@ -118,28 +120,37 @@ backtrace_for_thread(pid_t threadid, TextBuffer &text) Dbg(dbg_ctl_backtrace, "waited for target %ld, found PID %ld, %s\n", (long)threadid, (long)target, WIFSTOPPED(status) ? "STOPPED" : "???"); if (target < 0) { + text.format(" [waitpid failed: %s (%d)]\n", strerror(errno), errno); goto done; } ap = _UPT_create(threadid); Dbg(dbg_ctl_backtrace, "created UPT %p", ap); if (ap == nullptr) { + text.format(" [_UPT_create failed]\n"); goto done; } addr_space = unw_create_addr_space(&_UPT_accessors, 0 /* byteorder */); Dbg(dbg_ctl_backtrace, "created address space %p\n", addr_space); if (addr_space == nullptr) { + text.format(" [unw_create_addr_space failed]\n"); goto done; } status = unw_init_remote(&cursor, addr_space, ap); Dbg(dbg_ctl_backtrace, "unw_init_remote(...) -> %d\n", status); if (status != 0) { + text.format(" [unw_init_remote failed: %d]\n", status); goto done; } - while (unw_step(&cursor) > 0) { + step_result = unw_step(&cursor); + if (step_result <= 0) { + text.format(" [unw_step returned %d on first call]\n", step_result); + } + + while (step_result > 0) { unw_word_t ip; unw_word_t offset; char buf[256]; @@ -147,8 +158,8 @@ backtrace_for_thread(pid_t threadid, TextBuffer &text) unw_get_reg(&cursor, UNW_REG_IP, &ip); if (unw_get_proc_name(&cursor, buf, sizeof(buf), &offset) == 0) { - int status; - char *name = abi::__cxa_demangle(buf, nullptr, nullptr, &status); + int demangle_status; + char *name = abi::__cxa_demangle(buf, nullptr, nullptr, &demangle_status); text.format("%-4u 0x%016llx %s + %p\n", level, static_cast(ip), name ? name : buf, (void *)offset); free(name); } else { @@ -156,6 +167,7 @@ backtrace_for_thread(pid_t threadid, TextBuffer &text) } ++level; + step_result = unw_step(&cursor); } done: @@ -167,8 +179,10 @@ backtrace_for_thread(pid_t threadid, TextBuffer &text) _UPT_destroy(ap); } - status = ptrace(PTRACE_DETACH, target, NULL, DATA_NULL); - Dbg(dbg_ctl_backtrace, "ptrace(DETACH, %ld) -> %d (errno %d)\n", (long)target, status, errno); + if (target > 0) { + status = ptrace(PTRACE_DETACH, target, NULL, DATA_NULL); + Dbg(dbg_ctl_backtrace, "ptrace(DETACH, %ld) -> %d (errno %d)\n", (long)target, status, errno); + } } } // namespace int diff --git a/src/traffic_crashlog/traffic_crashlog.cc b/src/traffic_crashlog/traffic_crashlog.cc index 9354c5f838c..308fd9a31db 100644 --- a/src/traffic_crashlog/traffic_crashlog.cc +++ b/src/traffic_crashlog/traffic_crashlog.cc @@ -94,29 +94,36 @@ crashlog_open(const char *path) extern int ServerBacktrace(unsigned /* options */, int pid, char **trace); bool -crashlog_write_backtrace(FILE *fp, pid_t pid, const crashlog_target &) +crashlog_write_backtrace(FILE *fp, pid_t pid, const crashlog_target &target) { - char *trace = nullptr; - int mgmterr; + char *trace = nullptr; + int const mgmterr = ServerBacktrace(0, static_cast(pid), &trace); // NOTE: sometimes we can't get a backtrace because the ptrace attach will fail with // EPERM. I've seen this happen when a debugger is attached, which makes sense, but it // can also happen without a debugger. Possibly in that case, there is a race with the // kernel locking the process information? - if ((mgmterr = ServerBacktrace(0, static_cast(pid), &trace)) != 0) { - fprintf(fp, "Unable to retrieve backtrace: %d\n", mgmterr); - return false; + if (mgmterr == 0 && trace != nullptr) { + // ServerBacktrace succeeded - this gives us all threads' backtraces. + fprintf(fp, "%s", trace); + free(trace); + return true; } - if (trace == nullptr) { - fprintf(fp, "Unable to retrieve backtrace: trace is null\n"); - return false; + // ServerBacktrace failed. Fall back to the in-process backtrace from the crashing thread. + if ((target.flags & CRASHLOG_HAVE_BACKTRACE) && !target.backtrace.empty()) { + fprintf(fp, "Crashing Thread Backtrace:\n%s", target.backtrace.c_str()); + return true; } - fprintf(fp, "%s", trace); - free(trace); - return true; + // No backtrace available from either source. + if (mgmterr != 0) { + fprintf(fp, "Unable to retrieve backtrace: ServerBacktrace returned %d\n", mgmterr); + } else { + fprintf(fp, "Unable to retrieve backtrace: no backtrace data available\n"); + } + return false; } void @@ -200,7 +207,6 @@ main(int /* argc ATS_UNUSED */, const char **argv) Note("crashlog started, target=%ld, debug=%s syslog=%s, uid=%ld euid=%ld", static_cast(target_pid), debug_mode ? "true" : "false", syslog_mode ? "true" : "false", (long)getuid(), (long)geteuid()); - ink_zero(target); target.pid = static_cast(target_pid); target.timestamp = timestamp(); @@ -219,6 +225,21 @@ main(int /* argc ATS_UNUSED */, const char **argv) Warning("received %zd of %zu expected thread context bytes", nbytes, sizeof(target.ucontext)); target.flags &= ~CRASHLOG_HAVE_THREADINFO; } + + // Read the in-process backtrace from the crashing thread. + uint32_t bt_len = 0; + nbytes = read(STDIN_FILENO, &bt_len, sizeof(bt_len)); + if (nbytes == static_cast(sizeof(bt_len)) && bt_len > 0 && bt_len < 1024 * 1024) { + target.backtrace.resize(bt_len); + nbytes = read(STDIN_FILENO, target.backtrace.data(), bt_len); + if (nbytes == static_cast(bt_len)) { + target.flags |= CRASHLOG_HAVE_BACKTRACE; + Note("received %u bytes of in-process backtrace", bt_len); + } else { + Warning("received %zd of %u expected backtrace bytes", nbytes, bt_len); + target.backtrace.clear(); + } + } } logname = crashlog_name(); diff --git a/src/traffic_crashlog/traffic_crashlog.h b/src/traffic_crashlog/traffic_crashlog.h index 086c1fcda3d..5f45ad90684 100644 --- a/src/traffic_crashlog/traffic_crashlog.h +++ b/src/traffic_crashlog/traffic_crashlog.h @@ -28,6 +28,8 @@ #include "tscore/Diags.h" #include "tscore/TextBuffer.h" +#include + // ucontext.h is deprecated on Darwin, and we really only need it on Linux, so only // include it if we are planning to use it. #if defined(__linux__) @@ -49,17 +51,22 @@ #endif #define CRASHLOG_HAVE_THREADINFO 0x1u +#define CRASHLOG_HAVE_BACKTRACE 0x2u struct crashlog_target { - pid_t pid; - siginfo_t siginfo; + pid_t pid{0}; + siginfo_t siginfo{}; #if defined(__linux__) - ucontext_t ucontext; + ucontext_t ucontext{}; #else - char ucontext; // just a placeholder ... + char ucontext{}; // just a placeholder ... #endif - struct tm timestamp; - unsigned flags; + struct tm timestamp { + }; + unsigned flags{0}; + + // In-process backtrace from the crashing thread. + std::string backtrace; }; bool crashlog_write_backtrace(FILE *, const crashlog_target &); diff --git a/src/traffic_server/Crash.cc b/src/traffic_server/Crash.cc index 5ee6791a0ec..d6ffbdab17e 100644 --- a/src/traffic_server/Crash.cc +++ b/src/traffic_server/Crash.cc @@ -28,6 +28,9 @@ #include "tscore/Version.h" #include "tscore/signals.h" +#include +#include + // ucontext.h is deprecated on Darwin, and we really only need it on Linux, so only // include it if we are planning to use it. #if defined(__linux__) @@ -167,6 +170,13 @@ crash_logger_invoke(int signo, siginfo_t *info, void *ctx) // a single memory block that we can just puke out. ATS_UNUSED_RETURN(write(crash_logger_fd, info, sizeof(siginfo_t))); ATS_UNUSED_RETURN(write(crash_logger_fd, static_cast(ctx), sizeof(ucontext_t))); + + // Send zero-length backtrace. We cannot safely generate a backtrace here + // because backtrace() can acquire locks (e.g., in the dynamic linker) that + // the crashing thread might be holding, causing a deadlock. The crash + // logger will get the backtrace via ptrace from a separate process instead. + uint32_t bt_len = 0; + ATS_UNUSED_RETURN(write(crash_logger_fd, &bt_len, sizeof(bt_len))); #endif close(crash_logger_fd); diff --git a/src/tscore/ink_stack_trace.cc b/src/tscore/ink_stack_trace.cc index e84ca8d554b..61680d7f7db 100644 --- a/src/tscore/ink_stack_trace.cc +++ b/src/tscore/ink_stack_trace.cc @@ -28,8 +28,17 @@ #include #include #include +#include +#include #include +#if __has_include() +#include +#define HAS_CXXABI 1 +#else +#define HAS_CXXABI 0 +#endif + #ifndef STDERR_FILENO #define STDERR_FILENO 2 #endif @@ -85,6 +94,110 @@ ink_backtrace(const int n) return symbol; } +namespace +{ +// Demangle a symbol name if possible. The caller must free the returned string +// if it is non-null and different from the input. +char * +demangle_symbol(const char *symbol) +{ +#if HAS_CXXABI + // Symbol format is typically: "binary(mangled_name+0x1234) [0xaddr]" + // We need to extract the mangled name between '(' and '+' or ')' + const char *start = strchr(symbol, '('); + if (start == nullptr) { + return nullptr; + } + start++; + + const char *end = strchr(start, '+'); + if (end == nullptr) { + end = strchr(start, ')'); + } + if (end == nullptr || end <= start) { + return nullptr; + } + + size_t len = end - start; + char *mangled = static_cast(malloc(len + 1)); + if (mangled == nullptr) { + return nullptr; + } + memcpy(mangled, start, len); + mangled[len] = '\0'; + + int status = 0; + char *demangled = abi::__cxa_demangle(mangled, nullptr, nullptr, &status); + free(mangled); + + if (status == 0 && demangled != nullptr) { + return demangled; + } + return nullptr; +#else + (void)symbol; + return nullptr; +#endif +} +} // anonymous namespace + +ssize_t +ink_stack_trace_dump_to_fd(int fd) +{ + void *stack[INK_STACK_TRACE_MAX_LEVELS + 1]; + memset(stack, 0, sizeof(stack)); + int btl = backtrace(stack, INK_STACK_TRACE_MAX_LEVELS); + if (btl <= 2) { + return 0; + } + + // Use backtrace_symbols_fd which is async-signal-safe (doesn't call malloc). + // Skip the first 2 frames (this function and its caller). + backtrace_symbols_fd(stack + 2, btl - 2, fd); + + // We can't easily know how many bytes were written by backtrace_symbols_fd, + // so return a positive value to indicate success. + return 1; +} + +void +ink_stack_trace_get(std::string &bt) +{ + bt.clear(); + + void *stack[INK_STACK_TRACE_MAX_LEVELS + 1]; + memset(stack, 0, sizeof(stack)); + int btl = backtrace(stack, INK_STACK_TRACE_MAX_LEVELS); + if (btl <= 2) { + return; + } + + // Skip the first 2 frames (this function and its caller). + char **symbols = backtrace_symbols(stack + 2, btl - 2); + if (symbols == nullptr) { + return; + } + + char line[1024]; + for (int i = 0; i < btl - 2; ++i) { + char *demangled = demangle_symbol(symbols[i]); + + if (demangled != nullptr) { + // Extract the address part from the original symbol. + const char *addr_start = strchr(symbols[i], '['); + const char *addr = addr_start ? addr_start : ""; + snprintf(line, sizeof(line), "%-4d %s %s\n", i, demangled, addr); + free(demangled); + } else { + snprintf(line, sizeof(line), "%-4d %s\n", i, symbols[i]); + } + + bt += line; + } + + free(symbols); +} + #else /* !TS_HAS_BACKTRACE */ void @@ -102,4 +215,16 @@ ink_backtrace(const int /* n */) return nullptr; } +ssize_t +ink_stack_trace_dump_to_fd(int /* fd */) +{ + return -1; +} + +void +ink_stack_trace_get(std::string &bt) +{ + bt.clear(); +} + #endif /* TS_HAS_BACKTRACE */