Skip to content

Add IPV4_PREFIX_BITS and IPV6_PREFIX_BITS options to keyed_by_ip#89270

Merged
alexey-milovidov merged 45 commits into
ClickHouse:masterfrom
adityachopra29:add-prefix-support
Jun 8, 2026
Merged

Add IPV4_PREFIX_BITS and IPV6_PREFIX_BITS options to keyed_by_ip#89270
alexey-milovidov merged 45 commits into
ClickHouse:masterfrom
adityachopra29:add-prefix-support

Conversation

@adityachopra29
Copy link
Copy Markdown
Contributor

@adityachopra29 adityachopra29 commented Oct 31, 2025

CREATE QUOTA q_1 KEYED BY IP_ADDRESS IPV4_PREFIX_BITS 24 IPV6_PREFIX_BITS 64 

or

CREATE QUOTA q_2 KEYED BY FORWARDED_IP_ADDRESS IPV4_PREFIX_BITS 24 IPV6_PREFIX_BITS 64 

Then, if q_2 is queried by, say a user with forwarded_ip_address = '1.2.3.4' first, then the quota_key generated :

curl -H 'X-Forwarded-For: 1.2.3.4' sS 'http://localhost:8123?user=test_user' -d "
SELECT quota_key
FROM system.quota_usage
WHERE quota_name = 'q_2'
FORMAT TSV;
"

will give 1.2.3.0

Changelog category (leave one):

  • New Feature

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

  • Adds IPV4_PREFIX_BITS and IPV6_PREFIX_BITS flags when Keying done by IP_ADDRESS or FORWARDED_IP_ADDRESS

Documentation entry for user-facing changes

  • Documentation is written (mandatory for new features)

Note

Medium Risk
Changes quota key derivation and SQL parsing/serialization for IP- and forwarded-IP-keyed quotas, which can alter how limits are shared/enforced across clients/subnets. Includes new system table columns and validation logic; regressions could affect quota correctness but scope is contained to quota handling.

Overview
Adds optional IPV4_PREFIX_BITS/IPV6_PREFIX_BITS settings to quotas keyed by ip_address or forwarded_ip_address, allowing quota buckets to be shared by masked subnet instead of full IP.

Extends the Quota entity, SQL parser/formatter (CREATE/ALTER/SHOW CREATE incl. ON CLUSTER), users.xml quota parsing, and system.quotas (new nullable columns) to persist/expose these settings, and updates QuotaCache key calculation to apply masking (with a fast path and safe fallback for malformed X-Forwarded-For). Adds validation to reject prefix bits on non-IP key types and clears stale prefix bits when key type changes.

Updates docs and tests, including new stateless coverage for subnet-masked quota keys and adjusted integration queries to avoid SELECT * breakage from the new system.quotas columns.

Reviewed by Cursor Bugbot for commit b193898. Bugbot is set up for automated code reviews on this repo. Configure here.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Oct 31, 2025

CLA assistant check
All committers have signed the CLA.

@alexey-milovidov alexey-milovidov added the can be tested Allows running workflows for external contributors label Oct 31, 2025
@clickhouse-gh
Copy link
Copy Markdown
Contributor

clickhouse-gh Bot commented Oct 31, 2025

Workflow [PR], commit [5a7dad2]

Summary:


AI Review

Summary

This PR adds optional IPV4_PREFIX_BITS and IPV6_PREFIX_BITS support for quotas keyed by ip_address and forwarded_ip_address, covering SQL parsing/formatting, users.xml, runtime quota-key masking, SHOW CREATE, system.quotas, docs, and focused tests. The current code addresses the prior review concerns I rechecked, including parser order, non-integer rejection, /0 masking, stale prefix bits on non-IP key changes, XML parity, forwarded IPv6 and IPv4-mapped behavior, ON CLUSTER formatting, and system.quotas column ordering.

Missing context / blind spots
  • ⚠️ I did not run the new stateless or integration tests locally. GitHub checks still show Fast test, Build (arm_tidy), and the aggregate PR report pending; a completed green CI run would close this.
Final Verdict

Status: ✅ Approve

@clickhouse-gh clickhouse-gh Bot added the pr-feature Pull request with new product feature label Oct 31, 2025
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
Signed-off-by: Aditya Chopra <adityachopra2912@gmail.com>
@adityachopra29
Copy link
Copy Markdown
Contributor Author

Hi @alexey-milovidov, any update on this pr's review? I was not able to clear the formatting issues, if any, since there are already a lot of changes that arise besides my additions when I run clang-format. Please tell if any other changes are required.

@adityachopra29
Copy link
Copy Markdown
Contributor Author

Hi @alexey-milovidov, Is there any update on this PR? Please advise; I am keen to contribute further in ClickHouse. I will appreciate any feedback to merge this pr.

@alexey-milovidov
Copy link
Copy Markdown
Member

It didn't pass the style check.

@alexey-milovidov alexey-milovidov self-assigned this Jan 18, 2026
alexey-milovidov and others added 5 commits February 19, 2026 23:29
- Merge with origin/master to resolve conflicts (make_intrusive migration)
- Fix style check: remove unnecessary includes, fix trailing whitespace,
  fix missing space after `if`, fix Allman-style braces, use angle bracket
  includes instead of quotes, fix `//` comments to `///`
- Fix `std::optional<MaskBits>` default from `0` to `std::nullopt` in `Quota.h`
  so that quotas without explicit prefix bits don't spuriously show them
- Fix `formatKeyType` to handle `FORWARDED_IP_ADDRESS` in addition to `IP_ADDRESS`
- Fix `parseIpPrefixBits` to use `std::optional<MaskBits>` parameters so only
  actually parsed prefix bits are set, enabling independent ALTER of each
- Fix implicit UInt64→UInt8 truncation before range check in parser
- Update test reference file accordingly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The new `Nullable(UInt8)` columns expose the IP address prefix bits
configuration for quotas. They are NULL when not configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update `02117_show_create_table_system.reference` for new `ipv4_prefix_bits`
  and `ipv6_prefix_bits` columns in `system.quotas`
- Register `IPV4_PREFIX_BITS` and `IPV6_PREFIX_BITS` in the keyword enum
  instead of using `createDeprecated`
- Add prefix bits support for `keyed_by_forwarded_ip` in XML config
  (was only supported for `keyed_by_ip`)
- Fix test `01600_quota_by_prefix_forwarded_ip` to be robust against
  both IPv4 and IPv6 localhost connections
- Fix trailing whitespace in test script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The `test_quota` integration test used `SELECT * FROM system.quotas`
which broke after adding `ipv4_prefix_bits` and `ipv6_prefix_bits`
columns. Use explicit column names instead to avoid depending on the
full schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pufit pufit self-assigned this Mar 13, 2026
Copy link
Copy Markdown
Member

@pufit pufit left a comment

Choose a reason for hiding this comment

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

Nice feature — subnet-level quota keying is a solid addition. The core implementation (parser, serialization round-trip, system table, masking logic) looks good. Found a few issues though, one of which is a real semantic bug.

Bug: Prefix bits not cleared when key_type changes

InterpreterCreateQuotaQuery.cpp only sets prefix bits when the query specifies them — it never clears them:

if (query.ipv4_prefix_bits)
    quota.ipv4_prefix_bits = query.ipv4_prefix_bits;

This means changing key_type away from IP preserves stale bits:

CREATE QUOTA q KEYED BY ip_address IPV4_PREFIX_BITS 24 IPV6_PREFIX_BITS 64;
ALTER QUOTA q KEYED BY client_key;
-- SHOW CREATE hides the bits (formatter only shows them for IP key types)
-- But system.quotas still shows ipv4_prefix_bits=24, ipv6_prefix_bits=64

ALTER QUOTA q KEYED BY ip_address;  -- switch back, no bits specified
-- SHOW CREATE now shows IPV4_PREFIX_BITS 24 IPV6_PREFIX_BITS 64 — ghost values resurrected

The bits silently survive a round-trip through a non-IP key type. Fix: when query.key_type is set and is not IP_ADDRESS/FORWARDED_IP_ADDRESS, explicitly reset both:

if (query.key_type)
{
    quota.key_type = *query.key_type;
    if (*query.key_type != QuotaKeyType::IP_ADDRESS
        && *query.key_type != QuotaKeyType::FORWARDED_IP_ADDRESS)
    {
        quota.ipv4_prefix_bits.reset();
        quota.ipv6_prefix_bits.reset();
    }
}

Same issue in the parser: ALTER QUOTA q IPV4_PREFIX_BITS 16 (without KEYED BY) succeeds even if the quota is keyed by user_name. The bits get persisted on an incompatible key type — system.quotas shows them, SHOW CREATE hides them.

Minor: IPV4_PREFIX_BITS 0 is a no-op at runtime

QuotaCache.cpp has:

if (quota->ipv4_prefix_bits && *quota->ipv4_prefix_bits > 0)

So IPV4_PREFIX_BITS 0 is accepted, persisted, displayed in SHOW CREATE — but silently does nothing. A /0 prefix means "all IPs share one bucket" which is valid behavior. Either support it (remove the > 0 check) or reject it in the parser.

Tested all of the above against the arm_release CI build (bb26c10b).

@pufit
Copy link
Copy Markdown
Member

pufit commented Mar 17, 2026

Generated by Nerve

@adityachopra29
Copy link
Copy Markdown
Contributor Author

Hi, I will look into the errors and complete this pr shortly.

Comment thread src/Access/UsersConfigParser.cpp
Comment thread src/Access/QuotaCache.cpp
alexey-milovidov and others added 3 commits May 31, 2026 08:04
The SQL path rejects `IPV4_PREFIX_BITS`/`IPV6_PREFIX_BITS` on a quota whose
key type is not `ip_address` or `forwarded_ip_address`, but the `users.xml`
parser silently ignored the same elements for `keyed` or default quotas. Make
`UsersConfigParser` fail the config load instead, matching the SQL behaviour,
and cover one valid and one rejected XML case in the `test_quota` integration
test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `KEYED BY` syntax blocks and key lists omitted `forwarded_ip_address`
even though the prefix-bits paragraph below references it. Add it so the
forwarded-IP keying form is discoverable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…masking

`ALTER QUOTA q IPV4_PREFIX_BITS 16` without `KEYED BY` exercises the
`key_type == std::nullopt` branch of `ASTCreateQuotaQuery::formatImpl`, which
is the path used by `ON CLUSTER` distribution; add a round-trip regression for
it. Also add forwarded IPv6 masking coverage (`/64` and `/0`) so regressions in
the IPv6 branch are caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Access/QuotaCache.cpp
Comment thread src/Access/UsersConfigParser.cpp
@clickhouse-gh
Copy link
Copy Markdown
Contributor

clickhouse-gh Bot commented Jun 1, 2026

Dear @pufit, you haven't been active on this PR for 30 days. You will be unassigned. Will you continue working on it? If so, please feel free to reassign yourself.

alexey-milovidov and others added 3 commits June 1, 2026 18:34
An IPv4-mapped IPv6 address such as `::ffff:192.0.2.10` represents an IPv4
client, but `Poco::Net::IPAddress::family` reports it as IPv6, so it fell into
the IPv6 branch of `QuotaInfo::calculateKey` and was masked by `IPV6_PREFIX_BITS`
instead of `IPV4_PREFIX_BITS`. With a wide enough IPv6 prefix this collapsed all
mapped IPv4 clients into a single bucket, breaking the contract that IPv4 clients
are grouped by `IPV4_PREFIX_BITS`.

Normalize an IPv4-mapped address to native IPv4 before masking, so such clients
share quota buckets with plain IPv4 clients. Added a regression to
`01600_quota_by_prefix_forwarded_ip` exercising `::ffff:1.2.3.4` via the
forwarded-IP path, asserting it masks to `1.2.0.0`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `keyed_by_forwarded_ip` users.xml branch is a separate config path from
`keyed_by_ip` and from the SQL parser, but only `keyed_by_ip` had XML regression
coverage, and the XML quota guide did not mention the prefix-bit elements.

Add a valid `<keyed_by_forwarded_ip>` XML case with `<ipv4_prefix_bits>` /
`<ipv6_prefix_bits>` to the `test_quota` integration test, and document both
elements (and `<keyed_by_forwarded_ip />`) in `docs/en/operations/quotas.md`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adityachopra29
Copy link
Copy Markdown
Contributor Author

The failing test seems to be a borderline flaky-test, and looks like a job-timeout artifact.....rerunning the CI should fix this.

@alexey-milovidov
Copy link
Copy Markdown
Member

Merged the latest master into the branch (clean, no conflicts) to refresh the base and clear the chronic Server died CI flake on a fresh build.

I rebuilt locally on the merged base and verified the three affected tests against a fresh binary:

  • 01297_create_quota — matches the reference for all prefix-bits behavior, including the stale-bits-cleared fix (KEYED BY client_key then back to ip_address keeps ipv4/ipv6_prefix_bits as NULL), the /0 prefix, and the rejection cases. (The single ON CLUSTER line could only be skipped in my minimal local config because it has no ZooKeeper — it passes in CI.)
  • 01600_quota_by_prefix_forwarded_ip — full match, exercising the runtime subnet masking over HTTP with X-Forwarded-For.
  • 02117_show_create_table_system — the new ipv4_prefix_bits/ipv6_prefix_bits columns on system.quotas match.

All 15 review threads are resolved. The only CI failures are Server died, a chronic master-wide flake unrelated to this quota-only change (see #103235).

Comment thread src/Access/QuotaCache.cpp
alexey-milovidov and others added 2 commits June 6, 2026 00:36
…PV4_PREFIX_BITS

`mask_address` in `QuotaCache.cpp` normalized an IPv4-mapped IPv6 address
(such as `::ffff:192.0.2.10`) to native IPv4 before checking whether an IPv4
prefix was configured. This changed the quota key for existing
`KEYED BY ip_address` / `KEYED BY forwarded_ip_address` quotas with no prefix
bits: a client previously keyed as `::ffff:192.0.2.10` became `192.0.2.10`,
breaking the contract that quotas without prefix bits behave as before.

Now a mapped IPv4 address is treated as an IPv4 client governed solely by
`IPV4_PREFIX_BITS`: it is normalized and masked only when `IPV4_PREFIX_BITS`
is configured, and otherwise keeps its original representation. In particular,
`IPV6_PREFIX_BITS` no longer collapses such addresses.

Added a regression to `01600_quota_by_prefix_forwarded_ip` covering a
forwarded `::ffff:1.2.3.4` with only `IPV6_PREFIX_BITS` set, asserting the
key stays `::ffff:1.2.3.4`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alexey-milovidov
Copy link
Copy Markdown
Member

Continued work on this PR:

  • Addressed the remaining unresolved review thread (mask_address in QuotaCache.cpp): an IPv4-mapped IPv6 address (e.g. ::ffff:192.0.2.10) was being normalized to native IPv4 before checking whether IPV4_PREFIX_BITS was configured, which silently changed the quota key for existing IP-keyed quotas that have no prefix bits. It is now treated as an IPv4 client governed solely by IPV4_PREFIX_BITS: normalized and masked only when that prefix is set, otherwise kept as-is (so IPV6_PREFIX_BITS never collapses it). Added a 01600_quota_by_prefix_forwarded_ip regression for the no-IPV4_PREFIX_BITS case.
  • Merged master (the branch was ~695 commits behind with an 02117_show_create_table_system.reference conflict; resolved cleanly).

Verified locally against a server built from this branch:

  • 01600_quota_by_prefix_forwarded_ip passes, including the new mapped-address case.
  • ALTER QUOTA q IPV4_PREFIX_BITS 16 without KEYED BY round-trips through SHOW CREATE (the key_type == std::nullopt formatImpl branch).
  • Prefix bits are cleared when key_type changes away from IP (no ghost values resurrected).
  • Prefix bits on a non-IP key type are rejected with BAD_ARGUMENTS; IPV4_PREFIX_BITS 33 is rejected with SYNTAX_ERROR.
  • The ON CLUSTER part of 01297_create_quota requires ZooKeeper and is covered by CI.

Pushed bb91e9e9777 (fix + test) and the master merge.

…rg::alter

PR ClickHouse#106102 (merged 2026-06-05 17:43 UTC) refactored Iceberg::alter to
declare last_version and compression_method outside an if/else that
assigns them in both branches. Clang-tidy cppcoreguidelines-init-variables
flags the bare declarations and Build (arm_tidy) is built with
-warnings-as-errors, so the build fails for every PR whose CI ran on
master after this commit.

CIDB shows 30+ unrelated PRs hitting this in the last 2 days
(e.g. ClickHouse#89360, ClickHouse#103540, ClickHouse#106120, ClickHouse#106386, ClickHouse#106404, ClickHouse#106522, ClickHouse#105102,
ClickHouse#106590).

Initialize both variables to safe defaults at declaration. They are
unconditionally overwritten in both if and else branches before use,
so behavior is unchanged. The new defaults are only relevant if a
future code path skips both assignments, in which case last_version=0
and compression_method=CompressionMethod::None are sane no-op values
(the same defaults the old structured-binding form would produce
through default-construction of the destructured aggregate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit d72cac5)
Comment thread src/Storages/System/StorageSystemQuotas.cpp Outdated
…f system.quotas

Inserting the new `ipv4_prefix_bits` / `ipv6_prefix_bits` columns between
`keys` and `durations` shifted the positional schema of `system.quotas`, so
existing `SELECT *`, TSV/CSV, or tuple-unpacking consumers would read former
`durations` / `apply_to_*` values from shifted positions.

Move the two columns to the end, after `apply_to_except`, so named-column
users get the new data while positional consumers keep reading the old
columns from the same positions. Update the column accessors and fill logic
in `fillData` accordingly, and adjust the `02117_show_create_table_system`
reference and the `system.quotas` documentation to match the new order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Access/QuotaCache.cpp
@clickhouse-gh
Copy link
Copy Markdown
Contributor

clickhouse-gh Bot commented Jun 7, 2026

LLVM Coverage Report

Metric Baseline Current Δ
Lines 84.50% 84.50% +0.00%
Functions 92.30% 92.30% +0.00%
Branches 77.20% 77.20% +0.00%

Changed lines: Changed C/C++ lines covered by tests: 233/244 (95.49%) | Lost baseline coverage (was covered on master, now uncovered in this PR): 2 line(s) · Uncovered code

Full report · Diff report

namespace
{
void formatKeyType(const QuotaKeyType & key_type, WriteBuffer & ostr, const IAST::FormatSettings &)
void formatIpPrefixBits(const std::optional<MaskBits> & ipv4_prefix_bits,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@groeneai, only after I merge this PR, send another PR that will edit 'Ip' to 'IP' everywhere according to our code style.

@alexey-milovidov alexey-milovidov added this pull request to the merge queue Jun 8, 2026
Merged via the queue into ClickHouse:master with commit a73928e Jun 8, 2026
166 checks passed
@robot-clickhouse robot-clickhouse added the pr-synced-to-cloud The PR is synced to the cloud repo label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors pr-feature Pull request with new product feature pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants