19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

生成AI活用×無料SaaS:tracerouteを世界地図で見える化【IPinfo×geojson.io】

Posted at

はじめに

GMOコネクトの永田です。

暗号のおねえさん( https://qiita.com/yumi-sakemi )から唐突に「インターネット経路の可視化をしたい!」と言われて対応しましたので、今回可視化にあたり実施したことをまとめます。

tracerouteの結果だけだと、どこを経由しているかが直感的に少し分かりにくいため、IPinfo と GeoJSON を活用して、可視化していきます。

まとめ

  • IPinfoでIPから地理的情報(国、都市、位置情報など)がお手軽に取得可能
  • GeoJSONで、地理的情報を元に地図上への可視化もお手軽に可能
  • 昔に比べて、標準フォーマットやSaaSの普及、生成AIにより、ちょっと試したいことがお手軽に

前提知識

今回、活用する主要なツールは次の3つです。

  1. traceroute
  2. IPinfo
  3. GeoJSON

tracerouteとは

tracerouteはご家庭でも良く利用されており、改めての説明は不要だとは思いますが、指定したサーバーまでのインターネット経路を、IPやdomainで表示してくれます。

以下、Wikipediaの例です。
ある程度、どこを通っているか分かりますが、可読性はあまり高くないです。

$ traceroute ja.wikipedia.org
traceroute to ja.wikipedia.org (208.80.152.2), 30 hops max, 60 byte packets
 1 ntt.setup (192.168.1.1) 0.121 ms 0.134 ms 0.159 ms
 2 118.23.8.17 (118.23.8.17) 6.037 ms 6.577 ms 7.064 ms
 3 118.23.5.137 (118.23.5.137) 4.971 ms 5.388 ms 5.368 ms
 4 122.1.164.213 (122.1.164.213) 7.556 ms 9.341 ms 11.167 ms
 5 60.37.55.165 (60.37.55.165) 6.195 ms 6.151 ms 6.154 ms
 6 60.37.27.89 (60.37.27.89) 6.470 ms 5.355 ms 5.761 ms
 7 ae-5.r21.tokyjp01.jp.bb.gin.ntt.net (129.250.11.53) 5.790 ms 7.090 ms 6.670 ms
 8 as-2.r21.snjsca04.us.bb.gin.ntt.net (129.250.4.44) 114.218 ms 113.157 ms 113.279 ms
 9 equinixexchange.ir1.sanjose-ca.us.xo.net (206.223.116.85) 122.223 ms 122.167 ms 122.115 ms
10 vb2001.rar3.la-ca.us.xo.net (207.88.13.110) 125.873 ms 132.449 ms 125.843 ms
11 vb15.rar3.dallas-tx.us.xo.net (207.88.12.45) 149.345 ms 149.826 ms 149.540 ms
12 207.88.14.42.ptr.us.xo.net (207.88.14.42) 182.000 ms 182.215 ms 181.906 ms
13 w006.z207088246.xo.cnc.net (207.88.246.6) 187.915 ms 187.107 ms 187.082 ms
14 rr.pmtpa.wikimedia.org (208.80.152.2) 186.903 ms 186.520 ms 186.758 ms

IPinfoとは

IPアドレスを元に、地理情報やASNなどをAPIで取得できるサービスです。

IPinfo Liteが無料で、IPinfo Core以上が有料となります。
TopページのLiteのレスポンス例だと国レベルまでの情報なのですが、 Docsに記載例のあるAPIを呼び出してみると都市名と位置情報まで取得可能ですので、今回はこれを利用します。

❯ curl ipinfo.io/8.8.8.8/json
{
  "ip": "8.8.8.8",
  "hostname": "dns.google",
  "city": "Mountain View",
  "region": "California",
  "country": "US",
  "loc": "37.4056,-122.0775",
  "org": "AS15169 Google LLC",
  "postal": "94043",
  "timezone": "America/Los_Angeles",
  "readme": "https://ipinfo.io/missingauth",
  "anycast": true
}

GeoJSON とは

RFC7946 The GeoJSON Formatで規定されており、地理空間データをJSON形式で表現するための仕様です。

このGeoJSONを利用し、地図上に可視化できるサービスがあります。

Wikiのtracerouteを実際に可視化した例:

スクリーンショット 2025-11-05 15.28.28.png

では、これらを組み合わせて、経路の可視化をしていきましょう!

作成したプログラム

Copilot(Claude Haiku4.5)に依頼してtraceroute --> IPinfo --> GeoJSON出力の一連の処理をpythonスクリプトにしてもらいました。

src/traceroute_mapper.py: 長いので省略
#!/usr/bin/env python3
"""
Traceroute結果を地図上に可視化するツール
ipinfoからロケーション情報を取得し、GeoJSON形式で出力します
"""

import json
import re
import subprocess
import sys
import time
from typing import Dict, List, Optional
import requests


class TracerouteMapper:
    """Traceroute結果をGeoJSON形式にマップするクラス"""

    def __init__(self, rate_limit_delay: float = 0.5):
        """
        初期化

        Args:
            rate_limit_delay: ipinfoへのリクエスト間隔(秒)
        """
        self.rate_limit_delay = rate_limit_delay
        self.ipinfo_cache: Dict[str, Optional[Dict]] = {}

    def parse_traceroute(self, traceroute_output: str) -> List[Dict]:
        """
        Traceroute出力をパースしてホップ情報を抽出

        Args:
            traceroute_output: tracerouteコマンドの出力テキスト

        Returns:
            ホップ情報のリスト [{"hop": 2, "hostname": "...", "ip": "1.2.3.4", "times": [...]}]
        """
        hops = []
        lines = traceroute_output.strip().split('\n')

        for line in lines:
            # ホップ行をパース(例:" 2  hostname (IP) times")
            match = re.match(
                r'\s*(\d+)\s+([^\s]+)\s+\(([0-9.]+)\)\s+(.+)',
                line
            )
            if match:
                hop_num = int(match.group(1))
                hostname = match.group(2)
                ip = match.group(3)
                times_str = match.group(4)

                # 時間値を抽出(* は無視)
                times = []
                for time_val in re.findall(r'([\d.]+)\s*ms', times_str):
                    times.append(float(time_val))

                hops.append({
                    'hop': hop_num,
                    'hostname': hostname,
                    'ip': ip,
                    'times': times,
                    'avg_time': sum(times) / len(times) if times else 0
                })

        return hops

    def get_ipinfo(self, ip: str) -> Optional[Dict]:
        """
        ipinfo.ioからIPの位置情報を取得

        Args:
            ip: 対象のIPアドレス

        Returns:
            位置情報を含む辞書、またはエラー時はNone
        """
        if ip in self.ipinfo_cache:
            return self.ipinfo_cache[ip]

        try:
            time.sleep(self.rate_limit_delay)
            # ipinfo.io API エンドポイント (JSON レスポンス)
            response = requests.get(
                f'https://ipinfo.io/{ip}/json',
                timeout=10,
                headers={'User-Agent': 'curl/7.68.0'}
            )
            response.raise_for_status()
            
            try:
                data = response.json()
            except ValueError as e:
                print(f"Warning: Invalid JSON response for {ip}: {e}", file=sys.stderr)
                self.ipinfo_cache[ip] = None
                return None

            # loc フィールドから緯度経度を解析
            if 'loc' in data:
                try:
                    loc_parts = data['loc'].split(',')
                    if len(loc_parts) == 2:
                        data['lat'] = float(loc_parts[0])
                        data['lon'] = float(loc_parts[1])
                except ValueError:
                    pass

            self.ipinfo_cache[ip] = data
            
            # デバッグログ: ipinfo から取得したホップ情報を表示
            country = data.get('country', 'N/A')
            city = data.get('city', 'N/A')
            loc = data.get('loc', 'N/A')
            print(f"  [ipinfo] IP: {ip}, Country: {country}, City: {city}, Loc: {loc}", file=sys.stderr)
            
            return data

        except requests.RequestException as e:
            print(f"Warning: Failed to get location for {ip}: {e}", file=sys.stderr)
            self.ipinfo_cache[ip] = None
            return None
        except Exception as e:
            print(f"Warning: Unexpected error for {ip}: {e}", file=sys.stderr)
            self.ipinfo_cache[ip] = None
            return None

    def create_geojson_feature(
            self,
            hop: Dict,
            location: Optional[Dict],
            index: int
    ) -> Optional[Dict]:
        """
        ホップ情報をGeoJSONフィーチャに変換

        Args:
            hop: ホップ情報
            location: ロケーション情報
            index: ホップのインデックス

        Returns:
            GeoJSONフィーチャ、またはロケーション情報がない場合はNone
        """
        if not location or 'lat' not in location or 'lon' not in location:
            return None

        return {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [location['lon'], location['lat']]
            },
            "properties": {
                "hop": hop['hop'],
                "hostname": hop['hostname'],
                "ip": hop['ip'],
                "city": location.get('city', 'N/A'),
                "region": location.get('region', 'N/A'),
                "country": location.get('country', 'N/A'),
                "org": location.get('org', 'N/A'),
                "avg_response_time_ms": round(hop['avg_time'], 2),
                "response_times_ms": [round(t, 2) for t in hop['times']],
                "timezone": location.get('timezone', 'N/A')
            }
        }

    def create_geojson(
            self,
            hops: List[Dict],
            include_lines: bool = True
    ) -> Dict:
        """
        GeoJSON FeatureCollection を作成

        Args:
            hops: ホップ情報のリスト
            include_lines: ホップ間を線で結ぶかどうか

        Returns:
            GeoJSON FeatureCollection
        """
        features = []
        coordinates = []

        # ポイントフィーチャを作成
        for hop in hops:
            location = self.get_ipinfo(hop['ip'])
            feature = self.create_geojson_feature(hop, location, len(features))

            if feature:
                features.append(feature)
                coords = feature['geometry']['coordinates']
                coordinates.append(coords)

        # 線フィーチャを作成(オプション)
        if include_lines and len(coordinates) > 1:
            line_feature = {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": coordinates
                },
                "properties": {
                    "name": "Traceroute path",
                    "type": "path"
                }
            }
            features.append(line_feature)

        return {
            "type": "FeatureCollection",
            "features": features
        }
        # Note: FeatureCollection должен содержать только "type" и "features"
        # RFC 7946 准拠: FeatureCollectionレベルには "properties" を含めない

    def process_traceroute(self, traceroute_output: str) -> Dict:
        """
        Traceroute出力を処理してGeoJSONを返す

        Args:
            traceroute_output: tracerouteコマンドの出力

        Returns:
            GeoJSON FeatureCollection
        """
        print("Parsing traceroute output...", file=sys.stderr)
        hops = self.parse_traceroute(traceroute_output)
        print(f"Found {len(hops)} hops", file=sys.stderr)

        print("Fetching location information from ipinfo.io...", file=sys.stderr)
        geojson = self.create_geojson(hops)

        valid_hops = len([f for f in geojson['features'] if f['properties'].get('type') != 'path'])
        print(f"Successfully mapped {valid_hops} hops", file=sys.stderr)

        return geojson


def main():
    """メイン関数"""
    import argparse

    parser = argparse.ArgumentParser(
        description='Traceroute結果をGeoJSON形式に変換し、geojson.io向けにフォーマットします'
    )
    parser.add_argument(
        'host',
        nargs='?',
        help='Traceroute対象のホスト名またはIPアドレス'
    )
    parser.add_argument(
        '-f', '--file',
        help='Traceroute出力を含むファイルを指定'
    )
    parser.add_argument(
        '-o', '--output',
        help='出力ファイル名(デフォルト: traceroute.geojson)'
    )
    parser.add_argument(
        '--no-lines',
        action='store_true',
        help='ホップ間の線を描画しない'
    )
    parser.add_argument(
        '--rate-limit',
        type=float,
        default=0.5,
        help='ipinfoへのリクエスト間隔(秒、デフォルト: 0.5)'
    )

    args = parser.parse_args()

    # Traceroute出力を取得
    if args.file:
        with open(args.file, 'r', encoding='utf-8') as f:
            traceroute_output = f.read()
    elif args.host:
        print(f"Running traceroute to {args.host}...", file=sys.stderr)
        try:
            result = subprocess.run(
                ['traceroute', args.host],
                capture_output=True,
                text=True,
                timeout=60
            )
            traceroute_output = result.stdout
            if result.returncode != 0 and not traceroute_output:
                print("Error: traceroute failed", file=sys.stderr)
                sys.exit(1)
        except FileNotFoundError:
            print("Error: traceroute command not found", file=sys.stderr)
            sys.exit(1)
    else:
        # 標準入力から読み込み
        print("Reading traceroute output from stdin...", file=sys.stderr)
        traceroute_output = sys.stdin.read()

    if not traceroute_output.strip():
        print("Error: No traceroute output provided", file=sys.stderr)
        sys.exit(1)

    # 処理を実行
    mapper = TracerouteMapper(rate_limit_delay=args.rate_limit)
    geojson = mapper.process_traceroute(traceroute_output)

    # 出力
    output_file = args.output or 'traceroute.geojson'
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(geojson, f, indent=2, ensure_ascii=False)

    print(f"GeoJSON saved to {output_file}", file=sys.stderr)
    print("Open in geojson.io: https://geojson.io/", file=sys.stderr)


if __name__ == '__main__':
    main()
python src/traceroute_mapper.py -f test_wikipedia.txt -o 
output_wikipedia.geojson

のように呼び出すようです。(Copilot曰く)

経路の可視化をやってみた

ということで、実際にやってみます。

色々と経由するほうがデモの見栄えが良いので、日本から直接接続できなく複数のルーターを経由しそうなデンマークで試してみます。
(なおClaudflareとかのCDN Edgeに行って、いい感じの経路にならなかったページが多かったので、いくつか試行錯誤😇)

tracerouteの結果例

traceroute to nyidanmark.dk (188.64.159.75), 64 hops max, 40 byte packets
 1  softbank219188215188.bbtec.net (219.188.215.188)  10.345 ms  9.359 ms  8.484 ms
 2  * softbank221110200101.bbtec.net (221.110.200.101)  13.353 ms  8.766 ms
 3  10.0.61.45 (10.0.61.45)  10.586 ms  9.387 ms  10.264 ms
 4  10.9.201.2 (10.9.201.2)  257.910 ms
    10.9.201.34 (10.9.201.34)  284.139 ms  283.854 ms
 5  softbank221111203010.bbtec.net (221.111.203.10)  316.838 ms  282.120 ms  276.871 ms
 6  cph-sa2-i.cph.dk.net.dtag.de (62.154.14.158)  294.139 ms  287.058 ms  328.109 ms
 7  cph-sa2-i.cph.dk.net.dtag.de (62.154.14.158)  293.090 ms  293.662 ms  287.280 ms
 8  62.157.251.55 (62.157.251.55)  279.821 ms  278.420 ms  363.227 ms
 9  212.112.170.209 (212.112.170.209)  309.779 ms  281.451 ms  281.394 ms
10  lo1-taas11cr2dk.gc-net.eu (77.243.32.4)  304.401 ms  295.419 ms  289.330 ms
11  152.115.194.34 (152.115.194.34)  293.059 ms  292.513 ms  320.054 ms

IPinfoからの情報取得例

Parsing traceroute output...
Found 10 hops
Fetching location information from ipinfo.io...
  [ipinfo] IP: 219.188.215.188, Country: JP, City: Tokyo, Loc: 35.6895,139.6917
  [ipinfo] IP: 10.0.61.45, Country: N/A, City: N/A, Loc: N/A
  [ipinfo] IP: 10.9.201.2, Country: N/A, City: N/A, Loc: N/A
  [ipinfo] IP: 221.111.203.10, Country: NL, City: Amsterdam, Loc: 52.3740,4.8897
  [ipinfo] IP: 62.154.14.158, Country: DE, City: Hamburg, Loc: 53.5507,9.9930
  [ipinfo] IP: 62.157.251.55, Country: DE, City: Frankfurt am Main, Loc: 50.1155,8.6842
  [ipinfo] IP: 212.112.170.209, Country: SE, City: Malmö, Loc: 55.6059,13.0007
  [ipinfo] IP: 77.243.32.4, Country: DK, City: Copenhagen, Loc: 55.6759,12.5655
  [ipinfo] IP: 152.115.194.34, Country: DK, City: Copenhagen, Loc: 55.6759,12.5655
Successfully mapped 8 hops
GeoJSON saved to output_route_de.geojson
Open in geojson.io: https://geojson.io/

GeoJSONの例

nyidanmark.dkルートのGeoJSONの例(長いので省略)
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          139.6917,
          35.6895
        ]
      },
      "properties": {
        "hop": 1,
        "hostname": "softbank219188215188.bbtec.net",
        "ip": "219.188.215.188",
        "city": "Tokyo",
        "region": "Tokyo",
        "country": "JP",
        "org": "AS17676 SoftBank Corp.",
        "avg_response_time_ms": 9.4,
        "response_times_ms": [
          10.35,
          9.36,
          8.48
        ],
        "timezone": "Asia/Tokyo"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          4.8897,
          52.374
        ]
      },
      "properties": {
        "hop": 5,
        "hostname": "softbank221111203010.bbtec.net",
        "ip": "221.111.203.10",
        "city": "Amsterdam",
        "region": "North Holland",
        "country": "NL",
        "org": "AS17676 SoftBank Corp.",
        "avg_response_time_ms": 291.94,
        "response_times_ms": [
          316.84,
          282.12,
          276.87
        ],
        "timezone": "Europe/Amsterdam"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          9.993,
          53.5507
        ]
      },
      "properties": {
        "hop": 6,
        "hostname": "cph-sa2-i.cph.dk.net.dtag.de",
        "ip": "62.154.14.158",
        "city": "Hamburg",
        "region": "Hamburg",
        "country": "DE",
        "org": "AS3320 Deutsche Telekom AG",
        "avg_response_time_ms": 303.1,
        "response_times_ms": [
          294.14,
          287.06,
          328.11
        ],
        "timezone": "Europe/Berlin"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          9.993,
          53.5507
        ]
      },
      "properties": {
        "hop": 7,
        "hostname": "cph-sa2-i.cph.dk.net.dtag.de",
        "ip": "62.154.14.158",
        "city": "Hamburg",
        "region": "Hamburg",
        "country": "DE",
        "org": "AS3320 Deutsche Telekom AG",
        "avg_response_time_ms": 291.34,
        "response_times_ms": [
          293.09,
          293.66,
          287.28
        ],
        "timezone": "Europe/Berlin"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          8.6842,
          50.1155
        ]
      },
      "properties": {
        "hop": 8,
        "hostname": "62.157.251.55",
        "ip": "62.157.251.55",
        "city": "Frankfurt am Main",
        "region": "Hesse",
        "country": "DE",
        "org": "AS3320 Deutsche Telekom AG",
        "avg_response_time_ms": 307.16,
        "response_times_ms": [
          279.82,
          278.42,
          363.23
        ],
        "timezone": "Europe/Berlin"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          13.0007,
          55.6059
        ]
      },
      "properties": {
        "hop": 9,
        "hostname": "212.112.170.209",
        "ip": "212.112.170.209",
        "city": "Malmö",
        "region": "Skåne",
        "country": "SE",
        "org": "AS12552 GlobalConnect AB",
        "avg_response_time_ms": 290.87,
        "response_times_ms": [
          309.78,
          281.45,
          281.39
        ],
        "timezone": "Europe/Stockholm"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          12.5655,
          55.6759
        ]
      },
      "properties": {
        "hop": 10,
        "hostname": "lo1-taas11cr2dk.gc-net.eu",
        "ip": "77.243.32.4",
        "city": "Copenhagen",
        "region": "Capital Region",
        "country": "DK",
        "org": "AS42525 GlobalConnect A/S",
        "avg_response_time_ms": 296.38,
        "response_times_ms": [
          304.4,
          295.42,
          289.33
        ],
        "timezone": "Europe/Copenhagen"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          12.5655,
          55.6759
        ]
      },
      "properties": {
        "hop": 11,
        "hostname": "152.115.194.34",
        "ip": "152.115.194.34",
        "city": "Copenhagen",
        "region": "Capital Region",
        "country": "DK",
        "org": "AS31027 GlobalConnect A/S",
        "avg_response_time_ms": 301.88,
        "response_times_ms": [
          293.06,
          292.51,
          320.05
        ],
        "timezone": "Europe/Copenhagen"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [
            139.6917,
            35.6895
          ],
          [
            4.8897,
            52.374
          ],
          [
            9.993,
            53.5507
          ],
          [
            9.993,
            53.5507
          ],
          [
            8.6842,
            50.1155
          ],
          [
            13.0007,
            55.6059
          ],
          [
            12.5655,
            55.6759
          ],
          [
            12.5655,
            55.6759
          ]
        ]
      },
      "properties": {
        "name": "Traceroute path",
        "type": "path"
      }
    }
  ]
}

geojson.ioで可視化した例

スクリーンショット 2025-11-05 16.04.21.png

アムステルダムから色々とトランジットをがんばってコペンハーゲンまで到達しているのが、分かりやすくなりました!
(飛行機のトランジットのようです)

昔はこのようなことをやろうとしても、IPからの位置情報がなかったり、地図の可視化もMaP API調べたりと大変だったのですが、RFCでの標準化やSaaSが増えてきて、すぐに実現できるほどお手軽になりましたね😊

(再掲)まとめ

  • IPinfoでIPから地理的情報(国、都市、位置情報など)がお手軽に取得可能
  • GeoJSONで、地理的情報を元に地図上への可視化もお手軽に可能
  • 昔に比べて、標準フォーマットやSaaSの普及、生成AIにより、ちょっと試したいことがお手軽に

最後に、GMOコネクトでは研究開発や国際標準化に関する支援や技術検証をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ: https://gmo-connect.jp/contactus/

19
3
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
19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?