0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「10.0.0.12 を含むか」で内部アクセスを判定していたら、110.0.0.123 まで内部になっていた

0
Posted at

背景

自分のサイト群のアクセスを、LAN 内に置いた自前のダッシュボードで集計しています。
そのダッシュボードから各サイトのリンクを踏むと、アクセス解析に「参照元 10.0.0.12」として記録されてしまうので、集計の側で「内部のアクセス」として別に数えていました。
その判定が、思っていたよりずっと雑でした…
2026-09-29 に直したので、その備忘録です。

直す前の判定

アクセス解析からは、参照元とメディアを 10.0.0.12:8089 / referral のような1つの文字列で受け取っています。
/ の左が参照元のホスト、右が流入の種類です。
直す前は、これを下記の関数で判定していました。

const INTERNAL_REFERRER_HOSTS = ["10.0.0.12", "127.0.0.1", "localhost"];

function isInternalReferrer(sourceMedium) {
  const s = String(sourceMedium == null ? "" : sourceMedium).toLowerCase();
  return INTERNAL_REFERRER_HOSTS.some((h) => s.includes(h));
}

文字列の中に 10.0.0.12 が含まれていれば内部、というだけの仕組みだ。
書いた当時は、ダッシュボードの機械1台だけを見ればいいと思っていました。

何がまずかったか

部分一致なので、関係ない文字列まで拾います。
110.0.0.123 は先頭の1と末尾の3を除くと 10.0.0.12 なので、外部の IP なのに内部扱いになる。
localhost-blog.example.com のようなホスト名も localhost を含むので同じです。
逆に、同じ LAN の別の機械(192.168.1.20 など)や、IPv6 のループバック [::1] から踏んだ分は、リストに無いので外部として数えていました。

直した後の判定

方針は2つです。
1つ目は、部分一致をやめて、ポートを外したホスト部分だけを取り出して比べること。
2つ目は、特定の1台ではなく「プライベートな IP 全般」を内部とみなすこと。

プライベート IP は、組織の中で自由に使ってよいと RFC 1918 で決められた3つの範囲(10.0.0.0/8・172.16.0.0/12・192.168.0.0/16)のことだ。
インターネット上には出てこないアドレスなので、外部の読者のブラウザがこれを参照元として送ってくることはありません。
自分以外にここから来る人はいない、という理屈で内部に寄せています。

下記が直した後の判定です。
isPrivateHost はポートを外した後のホスト文字列を受け取ります。

function isPrivateHost(host) {
  const h = String(host || "").toLowerCase().replace(/^\[|\]$/g, "");
  if (!h) return false;
  if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local")) return true;
  if (h === "::1") return true;
  const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
  if (!m) return false;
  const [a, b] = [Number(m[1]), Number(m[2])];
  return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
}

function isInternalReferrer(sourceMedium) {
  const s = String(sourceMedium == null ? "" : sourceMedium).toLowerCase();
  const source = s.split(" / ")[0].trim();
  // "[::1]:3000" / "10.0.0.12:8089" → ポートを外す(IPv6 は角括弧の中だけ残す)
  const host = source.startsWith("[")
    ? source.slice(0, source.indexOf("]") + 1)
    : (source.match(/:/g) || []).length === 1 ? source.replace(/:\d+$/, "") : source;
  return INTERNAL_REFERRER_HOSTS.includes(host) || isPrivateHost(host);
}

172.16.0.0/12 は 172.16 から 172.31 までなので、2つ目のオクテットを数値にして範囲で見ています。
文字列のままだと 172.3 と 172.32 の区別が付かないので、ここを数値にするのが肝だと思います。
127 はループバック(自分自身)で、RFC 1918 ではないものの、開発機から踏んだ分なので内部に入れました。
INTERNAL_REFERRER_HOSTS は直す前と同じ配列で、今は includes で完全一致だけを見ています。

実際に流してみた

新旧の判定に同じ入力を流した結果が下記です。

入力                                     旧     新
10.0.0.12:8089 / referral                true   true
192.168.1.20:5173 / referral             false  true
110.0.0.123 / referral                   true   false
172.32.0.1 / referral                    false  false
localhost-blog.example.com / referral    true   false
[::1]:3000 / referral                    false  true
google / organic                         false  false

110.0.0.123 と localhost-blog.example.com が内部から外れ、192.168.1.20 と [::1] が内部に入りました。
172.32.0.1 は 172.16.0.0/12 の外なので、新しい判定でも外部のままです。

範囲の境目を手で確かめたいときは、私はサブネット計算のツールに IP を入れて「プライベート (RFC1918)」の表示が出るかを見ています。

こちらは RFC 1918 の3範囲だけを見るので、127.0.0.1 はプライベートとは出ません。
ダッシュボードの判定はループバックも足しているので、そこだけ結果が違います。

IP アドレスを判定するときは、文字列のまま比べずに、ホストを切り出してオクテットを数値にしてから範囲で比べる。
これだけで、部分一致の取り違えはまとめて消せます。


本記事はAI補助で執筆した、個人開発の紹介記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?