192 Commits
Author SHA1 Message Date
NetworkCatsandClaude Opus 5.5 036a3fa061 Use mimalloc for request-path allocations
The production image is built against musl, whose allocator is slow and
takes a global lock. Profiling /json on a 1 CPU / 1 GB Alpine container
showed malloc/free at ~11% of total CPU, the largest user-space cost;
the rest is mostly the kernel network stack, already at one recvfrom
and one writev per request.

mimalloc alone, however, kept the freed ~96 MB MMDB buffer resident
after a database update (RSS stayed at ~197 MB in two of three swaps),
and MIMALLOC_PURGE_DELAY=0 fixes that only by giving back most of the
speedup. So blocks of 1 MiB or more, in practice only the MMDB file
contents, go to the system allocator, which unmaps them on free; RSS
returns to ~98 MB after every swap, as before.

Median of three alternating 20 s runs: /json across random IPs rose
from 131.6k to 142.4k rps (+8%), p50 latency 0.465ms -> 0.443ms; a
single IP rose from 124.5k to 127.4k rps (+2%). Steady-state RSS grows
by ~12 MB. Responses are byte-identical to before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v1.0.9
2026-10-23 19:16:07 +08:00
NetworkCatsandClaude Opus 5.5 c05b71e33b Raise rate limits
Each IP may now make 120 requests per minute instead of 60. Under
load the limit drops to 60 RPM once the site reaches 600 RPS and to
30 RPM at 1000 RPS, instead of 30 and 15 RPM at 200 and 300 RPS.

One page load makes three or four requests, and many visitors share
an IP behind carrier-grade NAT or an office gateway, so the old
limits throttled real users. The old global thresholds were also far
below what the host can serve. Update the FAQ in every locale to
match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:33:39 +08:00
NetworkCatsandClaude Opus 5.5 32ceddcc03 Load the database off the async runtime
Loading an MMDB reads the whole ~100 MB file with a blocking
std::fs::read. The updater did that directly on a tokio worker, and the
1 vCPU production host runs a single worker, so every scheduled update
stalled all in-flight requests until the load finished. With one
request every 2 ms on a single-worker runtime, the worst latency during
a reload was 31 ms; it is 1.7 ms after this change.

Run the load on the blocking thread pool, both for scheduled updates
and for the startup load in init_db.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:26:52 +08:00
NetworkCats b93dd972e7 Update .gitignore 2026-10-23 18:17:34 +08:00
NetworkCatsandClaude Opus 5.5 b8c5a7439c Fix fail2ban and disable SSH password logins
fs.nr_open = 65536 sat below systemd's default hard NOFILE limit
(524288), so any service without its own LimitNOFILE failed to restart
once the sysctl was applied (205/LIMITS); fail2ban was down after the
last deploy, and after a reboot ssh could be too. Leave nr_open at the
kernel default.

fail2ban also read /var/log/auth.log, which Debian 13 no longer
writes; read sshd's journal instead.

Disable password and keyboard-interactive SSH logins and allow root
with keys only. The drop-in is named 10-* so it wins over cloud-init's
50-cloud-init.conf, which turns password logins on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:58:38 +08:00
NetworkCatsandClaude Opus 5.5 712a82698e Fix Docker failing to start on first deploy
The firewall role's "Reload systemd" handler shadowed the docker role's
handler of the same name, and its flush_handlers ran the pending Docker
restart right after base lowered fs.nr_open, without a daemon-reload.
Docker then started without its LimitNOFILE override and systemd could
not apply the default limit (status=205/LIMITS).

Reload systemd inline in the firewall role instead of through a
handler, so other roles' handlers keep running at the end of the play,
and reload before ensuring Docker is running so a host left in this
state recovers on the next deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v1.0.8
2026-10-22 21:41:25 +08:00
NetworkCatsandClaude Opus 5.5 cd46024a46 Restrict HTTPS to Cloudflare with UFW on the host
The Vultr firewall now only opens 443 (and SSH); the host decides who may
use it. Docker-published ports bypass UFW's INPUT rules, so the new
firewall role routes Docker traffic through UFW's route rules via the
DOCKER-USER chain and allows Cloudflare's IPv4 ranges on 443 with
`ufw route allow`. ufw-cloudflare-sync keeps those rules in step with
https://www.cloudflare.com/ips-v4 daily via a systemd timer, refusing
to touch the rules if the list looks wrong.

The role runs on every deploy, before HAProxy starts, and fails the
deploy if the filtering is not in place, since HAProxy trusts
CF-Connecting-IP from anything that reaches it. The plain UFW allows
for 80/443 are dropped from the base role as they never applied to
Docker-published ports.

Rotating the deploy SSH key now rebuilds the instance, because Vultr
only installs SSH keys at creation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v1.0.7
2026-10-22 21:13:48 +08:00
NetworkCatsandClaude Opus 5.5 8c8b4051fb Speed up the /json API
Cache the serialized /json fields per MMDB record. Every field except
`ip` depends only on the record, and many IPs share one record, so the
response is now the cached fields with the address prepended. The cache
is keyed by data-section offset, capped at 16 MiB, and owned by the
loaded database so a hot-swapped DB never serves stale entries. A hit
costs ~0.27us instead of ~2.5us for decode + serialize; a miss is no
slower than before thanks to a pre-sized serialization buffer.

Trim per-request framework overhead shared by all endpoints:
- Dispatch by Host with a plain HostRouter service instead of an outer
  Router wrapping a service_fn, skipping a second route match and box.
- Apply security headers via tower's MapResponseLayer instead of
  middleware::from_fn, and reserve header capacity up front.
- Add a RequestedIp extractor that reads headers in place instead of
  cloning the whole HeaderMap.

On a 1 CPU / 1 GB Alpine (musl) container, /json throughput rose from
76.5k to 123.5k rps for a single IP (+61%) and from 84.9k to 130.5k rps
across 20k random IPs (+54%); p50 latency fell from 0.85ms to 0.52ms.
Responses are byte-identical to before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:55:38 +08:00
NetworkCats 223eab2be9 Deploy Vultr High Frequency instance in SJC v1.0.5 v1.0.6 2026-07-29 08:56:21 +08:00
NetworkCats 3298ba56eb Optimize MMDB response paths v1.0.3 v1.0.4 2026-05-25 18:55:23 +08:00
NetworkCats 6405e6aaa3 Fix IP validation and MMDB lookup handling 2026-05-25 18:40:51 +08:00
NetworkCatsandClaude Opus 4.7 4ba00fadf5 feat: poll MMDB every 6 hours to mitigate upstream release drift
The daily scheduled update at 01:20 UTC was systematically lagging the
upstream mergedipdb release by 24h. The upstream cron nominally fires
at 01:00 UTC but GitHub Actions schedule latency shifts the actual
publish time anywhere from ~01:05 to ~06:00+ UTC, so any fixed
single-daily time misses the current release on delayed days.

Introduce DB_UPDATE_INTERVAL_HOURS (default 6, must divide 24 evenly),
shift the anchor DB_UPDATE_TIME_UTC default to 00:20, and replace
secs_until_next with secs_until_next_tick that schedules at anchor +
N*interval. Also set RUST_LOG=rustyip=info,warn in the Ansible env
template so the scheduler and "database updated successfully (N bytes)"
info logs are actually visible in docker logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.0.2
2026-04-22 07:26:57 +00:00
NetworkCatsandClaude Opus 4.7 a17b1053ff test: relax proxy classification assertions in integration tests
The /proxy, /hosting, /tor, /vpn endpoints and the /json response embed
live MMDB classifications for 45.77.77.77 that flip as the Merged-IP
dataset is updated. Replace hard-coded expected values with shape checks
(boolean / "true"|"false") so the tests verify endpoint plumbing without
coupling to transient upstream data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.0.1
2026-04-21 07:38:56 +00:00
NetworkCatsandClaude Opus 4.7 794bed8690 test: remove brittle proxy classification assertion
The assertion that 45.77.77.77 is a Tor exit node depended on live MMDB
classifications that change as the Merged-IP dataset updates. The test
name promised "returns data for a known IP" — the remaining lookup_proxy
coverage already exercises the lookup path without pinning to transient
classifications.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 07:35:22 +00:00
NetworkCatsandClaude Opus 4.7 d1280a46ac feat: force fresh MMDB download on every deploy
Wipe the target color's database volume before starting the container so
each push re-downloads the latest Merged-IP.mmdb. Gate /health on DB
readiness (503 until loaded) so HAProxy blue-green only flips traffic
once the new backend has a usable DB, and raise the Ansible health-wait
window from 120s to 360s to cover the download.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 07:05:57 +00:00
NetworkCats 5e70e21296 fix: increase ssh-keyscan retries and wait for SSH port before scanning v1.0.0 2026-04-03 21:58:29 +08:00
NetworkCats afb5bd4c9b downgrade to Vultr Regular Performance 1vCPU/1GB and tighten resource limits
Switch from High Frequency 2vCPU/2GB ($18/mo) to Regular Performance
1vCPU/1GB ($5/mo). Tighten HAProxy rate limits from 100/50/30 RPM to
60/30/15 RPM with lower global RPS thresholds (200/300 instead of
300/500). Reduce maxconn, socket buffers, file descriptor limits, and
Docker resource settings to fit within 1GB RAM constraints. Update all
user-facing content and translations across 14 locales.
2026-04-03 21:18:09 +08:00
NetworkCats 22516e095f Update db.rs 2026-03-23 19:59:56 +08:00
NetworkCats bde6b91a74 fix: update proxy test assertions to match current MMDB data
45.77.77.77 no longer has is_proxy=true in the upstream database;
it now reports is_tor=true instead.
2026-03-23 19:45:56 +08:00
NetworkCats ab66dba48c fix: self-host Simple Line Icons and improve info table spacing
Replace the cdnjs CDN dependency with locally hosted font files and a
minimal CSS file containing only the 4 icons used (check, minus,
magnifier, social-github). Remove the CDN preconnect links.

Also reduce the gap between the IP display and the info table divider,
fix vertical alignment of icon and text rows by switching to
vertical-align: middle, and override the icon line-height to match
table text.
2026-03-23 19:33:55 +08:00
NetworkCats 0ee38b44df fix: ensure city label is available for JS-rendered alt IP rows
When the primary IP lacks city data, the conditional <tr data-label="city">
row is absent from the DOM, so readLabels() cannot scrape the translated
label. Embed translated labels as data-l-* attributes on the <table> element
so they are always available regardless of which rows are rendered.
2026-03-23 18:23:42 +08:00
NetworkCats c2247fc4a6 fix: add explicit line-height to ensure consistent layout across languages 2026-03-22 19:28:50 +08:00
NetworkCats ead6ee47be fix: reduce spacing between info table labels and values 2026-03-22 19:01:12 +08:00
NetworkCats c15d7ad36a fix: apply rustfmt and replace map_or with is_some_and per clippy 2026-03-22 18:23:22 +08:00
NetworkCats 991fd289a2 fix: improve accessibility to meet WCAG 2.2 AA/AAA standards
- Remove disruptive alert() on error pages, rely on role="alert" banner
- Add skip navigation link for keyboard users (WCAG 2.4.1)
- Fix heading hierarchy on error page (sr-only status text in h1)
- Add "opens in a new tab" screen reader hint on external links
- Wrap language selector in form with noscript submit button (WCAG 3.2.2)
- Add aria-label to copy-to-clipboard buttons and footer landmarks
- Use h2 for alt-IP section to maintain single h1 per page
- Add persistent underline on links for non-color identification (WCAG 1.4.1)
- Fix footer attribution contrast ratio to ~5.5:1 (WCAG 1.4.3)
- Add aria-busy/role=status loading announcements for skeleton state
- Add translations for new accessibility strings across all 14 locales
- Update integration tests to assert role="alert" instead of alert()
2026-03-22 18:21:29 +08:00
NetworkCats b76410abf4 modernize JS to ES2022+ module and fix CSS logical property consistency
Rewrite script.js from ES5 (IIFEs, var, .then() chains) to modern ES2022+:
const/let, arrow functions, template literals, async/await, optional
chaining, for...of, .includes(), AbortSignal.timeout(), and DOM APIs
for dynamic HTML construction. Fix unhandled clipboard rejection, abort
timeout leak, and global scope pollution. Update script tag to
type="module" and fix CSS: top -> inset-block-start, flex-start -> start,
and indentation in media query block.
2026-03-22 18:01:05 +08:00
NetworkCats 5af03bf9b9 fix: PO parser escape order, proxy endpoint consistency, and accessibility bugs 2026-03-22 17:52:25 +08:00
NetworkCats ab7fb7c3c6 refactor: migrate templates to Askama custom i18n filter syntax
Replace all {{ t_field_name }} template variables with {{ "msgid"|t(locale) }}
filter calls across all 6 template files. This works with the #[askama::filter_fn]
defined in handlers/html.rs, which calls i18n::translate() directly.

Templates updated: index.html, error_alert.html, error_page.html,
_footer.html, _footer_lang.html, _search_form.html
2026-03-22 17:33:55 +08:00
NetworkCats 8fba47afd1 refactor: split models.rs into mmdb/api sub-modules with pub(crate) visibility
Split the 865-line models.rs into 3 focused files:
- models/mod.rs: conversion logic, helpers, re-exports, tests
- models/mmdb.rs: MMDB deserialization types (internal, pub(crate))
- models/api.rs: API serialization types (public) with From<MmdbProxy> impl

Mmdb types are now pub(crate) since they are internal deserialization details
that should not be part of the public API surface.
2026-03-22 17:33:40 +08:00
NetworkCats ee6ec1a28b refactor: split handlers.rs into focused sub-modules with DRY improvements
Split the 957-line handlers.rs into 4 focused files:
- handlers/mod.rs: shared state, IP resolution, non-browser request helper
- handlers/html.rs: HTML template rendering with Askama custom i18n filter,
  eliminating the Translations struct and ~250 lines of boilerplate
- handlers/api.rs: JSON/plain-text API endpoints with generic field_handler
  and proxy_field_handler helpers (consolidates 8 near-identical handlers)
- handlers/seo.rs: robots.txt, sitemap, OpenAPI, manifest

DRY improvements included:
- extract_ip_from_header() parameterized helper (deduplicates CF/IPv4 extraction)
- handle_non_browser_request() helper (deduplicates CLI/JSON early-return)
- append_ip_query() helper (deduplicates ?ip= redirect construction)
- field_handler/proxy_field_handler generics (consolidates 8 handlers)
- Askama #[filter_fn] custom filter for i18n (eliminates Translations struct)
2026-03-22 17:33:26 +08:00
NetworkCats 77410593f2 refactor: consolidate DB lookup boilerplate, UA detection, and polish error/module docs
Phase 1 (DRY):
- db.rs: extract with_reader() helper to eliminate repeated load-guard-lookup
  boilerplate across 6 lookup functions; use From<MmdbProxy> for ProxyInfo
- ua_detect.rs: extract matches_any_prefix() shared by both prefix-matching fns

Phase 4 (Quality polish):
- error.rs: implement Debug, Display, and std::error::Error on AppError
- Add module-level //! docs to all source modules
2026-03-22 17:33:09 +08:00
NetworkCats ccfa7decda feat: schedule DB updates at fixed daily UTC time instead of interval
Replace DB_UPDATE_INTERVAL_HOURS with DB_UPDATE_TIME_UTC (HH:MM format,
default 01:20) so the database update runs at a predictable time each day.
This ensures the update happens 20 minutes after the upstream Merged IP
Data release at 01:00 UTC.
2026-03-22 16:43:02 +08:00
NetworkCats cefdceab2c feat: add dismissible noscript notification banner
Show a fixed bottom banner for users with JavaScript disabled,
using CSS checkbox hack for dismissal without requiring JS.
2026-03-22 16:30:30 +08:00
NetworkCats bf3d96ac9c fix: use semantic data-label keys for alt-IP table row labels
The JS used fragile numeric indices to look up localized labels from the
server-rendered table. When optional rows (ASN, Org, Country, City) were
missing for the primary IP, the indices shifted, causing 'Proxy' to appear
as the City label. Add data-label attributes to each <tr> in the template
and key the label map by name instead of position.
2026-03-22 16:23:14 +08:00
NetworkCats d078859c5c relax rate limits: 100/50/30 RPM tiers at 0/300/500 RPS thresholds 2026-03-22 14:13:12 +08:00
NetworkCats 4877724408 fix: dynamically scale down IPv6 address font size based on length 2026-03-22 12:34:26 +08:00
NetworkCats 66a3db8901 style: restore import order to satisfy cargo fmt 2026-03-22 12:20:27 +08:00
NetworkCats 41016891eb fix: update proxy test assertions to match current MMDB data
45.77.77.77 no longer has is_hosting=true in the upstream database;
it now reports is_anonymous=true instead.
2026-03-22 12:17:20 +08:00
NetworkCats da8434662f feat: add skeleton loading state for alt-IP section when primary IP is IPv6
Show a shimmer skeleton placeholder while fetching the local IPv4
address via the IPv4-only domain. When the primary IP is already IPv4,
no loading state is displayed and the alt section fades in directly.
2026-03-22 12:06:56 +08:00
NetworkCats d236302b09 fix: skip alt-IP detection when querying another IP address
The detectAltIp() function was unconditionally fetching the viewer's own
IPv6/IPv4 address and displaying it alongside queried IP results. Pass
the is_query flag to the client via a data attribute and guard the
alt-IP detection so it only runs when viewing your own IP.
2026-03-22 11:58:44 +08:00
NetworkCats fb661933e8 fix(deploy): replace SIGHUP reload with container recreate to fix bind mount staleness
Docker file-level bind mounts track inodes, not paths. Ansible template
writes create new inodes, leaving the container reading orphaned files.
Replacing SIGHUP with force-recreate ensures fresh mounts every time,
eliminating the admin socket dependency entirely.
2026-03-20 09:40:56 +08:00
NetworkCats bd71dae443 fix(deploy): preserve bind mount inode with unsafe_writes for HAProxy config 2026-03-20 08:56:06 +08:00
NetworkCats 09f3b6bd72 fix(deploy): move HAProxy admin socket to /tmp for correct permissions
The named Docker volume created /var/run/haproxy owned by root:root,
preventing the haproxy user (uid 99) from creating the socket file.
2026-03-20 08:17:36 +08:00
NetworkCats f72cf8190b fix(deploy): retry admin socket command after HAProxy reload 2026-03-20 07:36:52 +08:00
NetworkCats 1c8f274453 refactor(deploy): use HAProxy admin socket instead of stats HTTP API 2026-03-20 07:01:09 +08:00
NetworkCats 258f68af5d fix(deploy): force server out of maintenance after HAProxy SIGHUP reload 2026-03-20 06:49:29 +08:00
NetworkCats ec91cd7738 refactor: remove FAQPage JSON-LD structured data block 2026-03-20 06:20:50 +08:00
NetworkCats f2a6678bf5 Fix live demo link to use HTTPS
Updated the live demo link to use HTTPS.
2026-03-16 11:42:08 +08:00
NetworkCats c2fc9d00f4 fix(ci): disable codecov auto-search to prevent false file detection 2026-03-11 05:58:55 +08:00
NetworkCats 797bb7c76a fix Fmt 2026-03-10 21:44:29 +00:00