yougotmapped 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ # Ingress Geography Finder
2
+
3
+ from yougotmapped.utils.network import get_hop_location
4
+
5
+
6
+ def find_ingress_geography(trace_result: dict, target_asn: int | None) -> dict:
7
+
8
+ if not trace_result or not target_asn:
9
+ return {
10
+ "available": False,
11
+ "reason": "missing_data",
12
+ }
13
+
14
+ hops = trace_result.get("hops", [])
15
+ previous_asn = None
16
+
17
+ for hop in hops:
18
+ if hop.get("private"):
19
+ continue
20
+
21
+ hop_asn = hop.get("asn")
22
+ hop_ip = hop.get("ip")
23
+
24
+ if hop_asn is None:
25
+ previous_asn = hop_asn
26
+ continue
27
+
28
+ if previous_asn != hop_asn and hop_asn == target_asn:
29
+ loc = get_hop_location(hop_ip)
30
+ if not loc:
31
+ return {
32
+ "available": False,
33
+ "reason": "geo_lookup_failed",
34
+ }
35
+
36
+ lat, lon = loc
37
+
38
+ return {
39
+ "available": True,
40
+ "ingress_hop": hop["hop"],
41
+ "ip": hop_ip,
42
+ "asn": hop_asn,
43
+ "latitude": lat,
44
+ "longitude": lon,
45
+ }
46
+
47
+ previous_asn = hop_asn
48
+
49
+ return {
50
+ "available": False,
51
+ "reason": "ingress_not_detected",
52
+ }
53
+
54
+
55
+ def format_ingress_result(result: dict) -> None:
56
+
57
+ if not result.get("available"):
58
+ print("Ingress geography could not be determined.")
59
+ return
60
+
61
+ print("Ingress Geography:")
62
+ print(f" Hop: {result['ingress_hop']}")
63
+ print(f" IP: {result['ip']}")
64
+ print(f" ASN: AS{result['asn']}")
65
+ print(
66
+ f" Coordinates:{result['latitude']:.3f}, {result['longitude']:.3f}"
67
+ )
@@ -0,0 +1,78 @@
1
+ # Jitter Utilities
2
+
3
+ from ping3 import ping
4
+ import statistics
5
+
6
+
7
+ def jitter_test(host: str, count: int = 20, timeout: float = 1.0) -> dict:
8
+
9
+ rtts = []
10
+
11
+ for _ in range(count):
12
+ try:
13
+ delay = ping(host, timeout=timeout)
14
+ if delay is not None:
15
+ rtts.append(delay * 1000) # ms
16
+ except Exception:
17
+ continue
18
+
19
+ sent = count
20
+ received = len(rtts)
21
+ lost = sent - received
22
+
23
+ if received < 2:
24
+ return {
25
+ "reachable": False,
26
+ "sent": sent,
27
+ "received": received,
28
+ "packet_loss_percent": round((lost / sent) * 100, 1),
29
+ }
30
+
31
+ median_rtt = statistics.median(rtts)
32
+
33
+ jitter_ms = statistics.mean(abs(rtt - median_rtt) for rtt in rtts)
34
+
35
+ min_rtt = round(min(rtts), 2)
36
+ max_rtt = round(max(rtts), 2)
37
+ median_rtt = round(median_rtt, 2)
38
+ jitter_ms = round(jitter_ms, 2)
39
+
40
+ if jitter_ms < 3:
41
+ stability = "stable"
42
+ elif jitter_ms < 10:
43
+ stability = "moderate"
44
+ else:
45
+ stability = "unstable"
46
+
47
+ return {
48
+ "reachable": True,
49
+ "sent": sent,
50
+ "received": received,
51
+ "packet_loss_percent": round((lost / sent) * 100, 1),
52
+ "rtt_ms": {
53
+ "min": min_rtt,
54
+ "median": median_rtt,
55
+ "max": max_rtt,
56
+ },
57
+ "jitter_ms": jitter_ms,
58
+ "stability": stability,
59
+ }
60
+
61
+
62
+ def format_jitter_result(result: dict) -> None:
63
+
64
+ if not result.get("reachable"):
65
+ print("Jitter test failed (insufficient replies).")
66
+ return
67
+
68
+ print(f"Packets: sent={result['sent']} received={result['received']}")
69
+ print(f"Packet loss: {result['packet_loss_percent']}%")
70
+
71
+ rtt = result["rtt_ms"]
72
+ print(
73
+ f"RTT (ms): min={rtt['min']} "
74
+ f"median={rtt['median']} max={rtt['max']}"
75
+ )
76
+
77
+ print(f"Jitter: {result['jitter_ms']} ms")
78
+ print(f"Stability: {result['stability'].upper()}")
@@ -1,66 +1,117 @@
1
- # utils/mapping.py
1
+ from pathlib import Path
2
2
  import folium
3
3
 
4
- def plot_ip_location(ip_data, color='red', map_object=None):
5
- if 'loc' not in ip_data:
6
- print("Location data not available.")
7
- return
4
+ DEFAULT_GEO_RADIUS_KM = 40 # IP geolocation uncertainty radius
8
5
 
9
- latitude, longitude = map(float, ip_data['loc'].split(','))
10
6
 
11
- m = map_object if map_object else folium.Map(location=[latitude, longitude], zoom_start=12)
7
+ def plot_ip_location(geo: dict):
12
8
 
9
+ lat = geo.get("latitude")
10
+ lon = geo.get("longitude")
11
+
12
+ if lat is None or lon is None:
13
+ return None
14
+
15
+ m = folium.Map(location=[lat, lon], zoom_start=6)
16
+
17
+ # Destination marker
13
18
  folium.Marker(
14
- [latitude, longitude],
15
- popup=f"IP: {ip_data.get('ip', 'N/A')}\nCity: {ip_data.get('city', 'N/A')}\nRegion: {ip_data.get('region', 'N/A')}\nCountry: {ip_data.get('country', 'N/A')}",
16
- icon=folium.Icon(color=color)
19
+ location=[lat, lon],
20
+ popup=f"IP: {geo.get('ip')}",
21
+ icon=folium.Icon(color="red", icon="info-sign"),
17
22
  ).add_to(m)
18
23
 
24
+ # Uncertainty circle
19
25
  folium.Circle(
20
- radius=10000,
21
- location=[latitude, longitude],
22
- color=color,
26
+ location=[lat, lon],
27
+ radius=DEFAULT_GEO_RADIUS_KM * 1000, # meters
28
+ color="red",
23
29
  fill=True,
24
- fill_color=color,
25
- fill_opacity=0.2,
26
- popup="Approximate Area"
30
+ fill_color="red",
31
+ fill_opacity=0.15,
32
+ popup="Approximate IP geolocation",
27
33
  ).add_to(m)
28
34
 
29
35
  return m
30
36
 
31
37
 
32
- def plot_multiple_ip_locations(ip_data_list):
33
- if not ip_data_list:
34
- print("No valid IP data to plot.")
35
- return
38
+ def plot_multiple_ip_locations(geos: list[dict]):
36
39
 
37
- center_data = next((d for d in ip_data_list if 'loc' in d), None)
38
- if not center_data:
39
- print("No valid location data found in input list.")
40
- return
40
+ coords = [
41
+ (g.get("latitude"), g.get("longitude"))
42
+ for g in geos
43
+ if g.get("latitude") is not None and g.get("longitude") is not None
44
+ ]
41
45
 
42
- latitude, longitude = map(float, center_data['loc'].split(','))
43
- m = folium.Map(location=[latitude, longitude], zoom_start=2)
46
+ if not coords:
47
+ return None
44
48
 
45
- for data in ip_data_list:
46
- if 'loc' not in data:
49
+ avg_lat = sum(lat for lat, _ in coords) / len(coords)
50
+ avg_lon = sum(lon for _, lon in coords) / len(coords)
51
+
52
+ m = folium.Map(location=[avg_lat, avg_lon], zoom_start=3)
53
+
54
+ for geo in geos:
55
+ lat = geo.get("latitude")
56
+ lon = geo.get("longitude")
57
+ if lat is None or lon is None:
47
58
  continue
48
- lat, lon = map(float, data['loc'].split(','))
59
+
49
60
  folium.Marker(
50
- [lat, lon],
51
- popup=f"IP: {data.get('ip', 'N/A')}\nCity: {data.get('city', 'N/A')}\nRegion: {data.get('region', 'N/A')}\nCountry: {data.get('country', 'N/A')}",
52
- icon=folium.Icon(color='red')
61
+ location=[lat, lon],
62
+ popup=f"IP: {geo.get('ip')}",
63
+ icon=folium.Icon(color="red", icon="info-sign"),
53
64
  ).add_to(m)
54
65
 
55
66
  folium.Circle(
56
- radius=10000,
57
67
  location=[lat, lon],
58
- color='red',
68
+ radius=DEFAULT_GEO_RADIUS_KM * 1000,
69
+ color="red",
70
+ fill=True,
71
+ fill_color="red",
72
+ fill_opacity=0.15,
73
+ ).add_to(m)
74
+
75
+ return m
76
+
77
+
78
+ def plot_traceroute_path(trace_result: dict, m):
79
+
80
+ hops = trace_result.get("hops", [])
81
+ prev = None
82
+
83
+ for hop in hops:
84
+ lat = hop.get("latitude")
85
+ lon = hop.get("longitude")
86
+
87
+ if lat is None or lon is None:
88
+ continue
89
+
90
+ point = [lat, lon]
91
+
92
+ # Hop marker (no circle)
93
+ folium.CircleMarker(
94
+ location=point,
95
+ radius=4,
96
+ color="blue",
59
97
  fill=True,
60
- fill_color='red',
61
- fill_opacity=0.2,
62
- popup="Approximate Area"
98
+ fill_opacity=0.9,
99
+ popup=f"Hop {hop.get('hop')} ({hop.get('ip')})",
63
100
  ).add_to(m)
64
101
 
65
- m.save('ip_geolocation_map.html')
66
- print("Map saved with multiple IPs as 'ip_geolocation_map.html'")
102
+ if prev:
103
+ folium.PolyLine(
104
+ locations=[prev, point],
105
+ color="blue",
106
+ weight=2,
107
+ opacity=0.6,
108
+ ).add_to(m)
109
+
110
+ prev = point
111
+
112
+
113
+ def save_map(m, filename: str) -> str:
114
+
115
+ path = Path(filename).resolve()
116
+ m.save(str(path))
117
+ return str(path)
@@ -0,0 +1,132 @@
1
+ import random
2
+ import time
3
+ from typing import Dict, Optional
4
+
5
+ try:
6
+ from scapy.all import IP, TCP, sr1, conf
7
+ SCAPY_AVAILABLE = True
8
+ conf.verb = 0
9
+ except Exception:
10
+ SCAPY_AVAILABLE = False
11
+
12
+ def _probe_mss(
13
+ host: str,
14
+ port: int,
15
+ mss: int,
16
+ timeout: float = 1.0,
17
+ ) -> bool:
18
+ sport = random.randint(1024, 65535)
19
+
20
+ pkt = (
21
+ IP(dst=host, flags="DF")
22
+ / TCP(
23
+ sport=sport,
24
+ dport=port,
25
+ flags="S",
26
+ seq=1000,
27
+ options=[("MSS", mss)],
28
+ )
29
+ )
30
+
31
+ resp = sr1(pkt, timeout=timeout)
32
+ if resp is None:
33
+ return False
34
+
35
+ if resp.haslayer(TCP):
36
+ flags = resp[TCP].flags
37
+ return (flags & 0x12) == 0x12 # SYN+ACK
38
+
39
+ return False
40
+
41
+
42
+ def _estimate_mss(
43
+ rtt_ms: Optional[float],
44
+ traceroute_ok: bool,
45
+ ) -> Dict:
46
+ if rtt_ms is None or not traceroute_ok:
47
+ return {
48
+ "mss": 1360,
49
+ "confidence": "low",
50
+ "reason": "mobile_or_cgnat_likely",
51
+ }
52
+
53
+ if rtt_ms > 40:
54
+ return {
55
+ "mss": 1360,
56
+ "confidence": "low",
57
+ "reason": "high_rtt_path",
58
+ }
59
+
60
+ if rtt_ms > 20:
61
+ return {
62
+ "mss": 1420,
63
+ "confidence": "medium",
64
+ "reason": "possible_tunneling",
65
+ }
66
+
67
+ return {
68
+ "mss": 1460,
69
+ "confidence": "medium",
70
+ "reason": "standard_ethernet_assumed",
71
+ }
72
+
73
+ def discover_mss(
74
+ host: str,
75
+ port: int = 443,
76
+ *,
77
+ median_rtt_ms: Optional[float] = None,
78
+ traceroute_ok: bool = True,
79
+ ) -> Dict:
80
+ MIN_MSS = 536
81
+ MAX_MSS = 1460
82
+
83
+ if SCAPY_AVAILABLE:
84
+ try:
85
+ if _probe_mss(host, port, MIN_MSS):
86
+ low = MIN_MSS
87
+ high = MAX_MSS
88
+ best = MIN_MSS
89
+
90
+ while low <= high:
91
+ mid = (low + high) // 2
92
+ if _probe_mss(host, port, mid):
93
+ best = mid
94
+ low = mid + 1
95
+ else:
96
+ high = mid - 1
97
+ time.sleep(0.05)
98
+
99
+ return {
100
+ "reachable": True,
101
+ "method": "tcp-mss",
102
+ "mss": best,
103
+ "confidence": "high",
104
+ }
105
+
106
+ except PermissionError:
107
+ pass
108
+ except Exception:
109
+ pass
110
+
111
+ est = _estimate_mss(median_rtt_ms, traceroute_ok)
112
+
113
+ return {
114
+ "reachable": True,
115
+ "method": "estimated",
116
+ "mss": est["mss"],
117
+ "confidence": est["confidence"],
118
+ "reason": est["reason"],
119
+ }
120
+
121
+
122
+ def format_mss_result(result: Dict) -> None:
123
+ if not result.get("reachable"):
124
+ print("MSS unavailable.")
125
+ return
126
+
127
+ print(f"TCP MSS: {result['mss']} bytes")
128
+ print(f"Method: {result['method'].upper()}")
129
+ print(f"Confidence: {result.get('confidence', 'unknown').upper()}")
130
+
131
+ if result.get("method") == "estimated":
132
+ print(f"Reason: {result.get('reason')}")
@@ -0,0 +1,81 @@
1
+ import platform
2
+ import subprocess
3
+
4
+
5
+ def _ping_df(host: str, payload_size: int, timeout: int = 1) -> bool:
6
+
7
+ system = platform.system()
8
+
9
+ if system == "Windows":
10
+ cmd = [
11
+ "ping",
12
+ "-n", "1",
13
+ "-f",
14
+ "-l", str(payload_size),
15
+ host,
16
+ ]
17
+ else:
18
+ cmd = [
19
+ "ping",
20
+ "-c", "1",
21
+ "-M", "do",
22
+ "-s", str(payload_size),
23
+ host,
24
+ ]
25
+
26
+ try:
27
+ result = subprocess.run(
28
+ cmd,
29
+ stdout=subprocess.DEVNULL,
30
+ stderr=subprocess.DEVNULL,
31
+ timeout=timeout,
32
+ )
33
+ return result.returncode == 0
34
+ except Exception:
35
+ return False
36
+
37
+
38
+ def discover_mtu(host: str) -> dict:
39
+ low = 1200
40
+ high = 1472 # Ethernet MTU (1500)
41
+ best = None
42
+
43
+ while low <= high:
44
+ mid = (low + high) // 2
45
+
46
+ if _ping_df(host, mid):
47
+ best = mid
48
+ low = mid + 1
49
+ else:
50
+ high = mid - 1
51
+
52
+ if best is None:
53
+ return {
54
+ "reachable": False,
55
+ }
56
+
57
+ mtu = best + 28 # add IP (20) + ICMP (8)
58
+
59
+ if mtu >= 1500:
60
+ path_type = "standard"
61
+ elif mtu >= 1400:
62
+ path_type = "pppoe / light tunneling"
63
+ else:
64
+ path_type = "vpn / heavy tunneling"
65
+
66
+ return {
67
+ "reachable": True,
68
+ "mtu": mtu,
69
+ "payload_size": best,
70
+ "path_type": path_type,
71
+ }
72
+
73
+
74
+ def format_mtu_result(result: dict) -> None:
75
+ if not result.get("reachable"):
76
+ print("MTU discovery failed (host unreachable or blocked).")
77
+ return
78
+
79
+ print(f"Path MTU: {result['mtu']} bytes")
80
+ print(f"Payload Size: {result['payload_size']} bytes")
81
+ print(f"Inference: {result['path_type'].upper()}")
@@ -1,34 +1,95 @@
1
- # utils/network.py
2
1
  import requests
3
- import ipaddress
4
2
  import socket
3
+ import ipaddress
4
+
5
+ def is_private_ip(ip: str) -> bool:
6
+ try:
7
+ return ipaddress.ip_address(ip).is_private
8
+ except ValueError:
9
+ return False
10
+
11
+
12
+ def is_valid_ip(value: str) -> bool:
13
+ try:
14
+ ipaddress.ip_address(value)
15
+ return True
16
+ except ValueError:
17
+ return False
5
18
 
19
+ def get_public_ip() -> str | None:
20
+ try:
21
+ r = requests.get("https://api.ipify.org", timeout=5)
22
+ r.raise_for_status()
23
+ return r.text.strip()
24
+ except Exception:
25
+ return None
6
26
 
7
- def get_public_ip():
27
+ def resolve_domain_to_ip(domain: str) -> str | None:
8
28
  try:
9
- response = requests.get('https://api.ipify.org')
10
- response.raise_for_status()
11
- return response.text
12
- except requests.exceptions.RequestException as e:
13
- print(f"Error fetching public IP: {e}")
29
+ return socket.gethostbyname(domain)
30
+ except socket.gaierror:
14
31
  return None
15
32
 
16
33
 
17
- def get_geolocation(ip_or_domain, api_token):
18
- url = f"https://ipinfo.io/{ip_or_domain}/json"
19
- params = {'token': api_token} if api_token else {}
34
+ def get_geolocation(target: str) -> dict | None:
35
+ ip = target
36
+
37
+ if not is_valid_ip(target):
38
+ ip = resolve_domain_to_ip(target)
39
+ if not ip:
40
+ return None
41
+
42
+ if is_private_ip(ip):
43
+ return {
44
+ "ip": ip,
45
+ "private": True,
46
+ "note": "Private / non-routable address",
47
+ }
48
+
20
49
  try:
21
- response = requests.get(url, params=params)
22
- response.raise_for_status()
23
- return response.json()
24
- except requests.exceptions.RequestException as e:
25
- print(f"Error fetching geolocation data: {e}")
50
+ r = requests.get(
51
+ f"https://ipwho.is/{ip}",
52
+ timeout=6,
53
+ )
54
+ data = r.json()
55
+ except Exception:
56
+ return None
57
+
58
+ if not data.get("success", False):
26
59
  return None
27
60
 
61
+ return {
62
+ "ip": ip,
63
+ "hostname": data.get("hostname"),
64
+ "org": data.get("connection", {}).get("isp"),
65
+ "country": data.get("country"),
66
+ "region": data.get("region"),
67
+ "city": data.get("city"),
68
+ "postal": data.get("postal"),
69
+ "latitude": data.get("latitude"),
70
+ "longitude": data.get("longitude"),
71
+ "timezone": data.get("timezone", {}).get("id"),
72
+ "raw": data,
73
+ }
74
+
75
+
76
+ def get_hop_location(ip: str) -> dict | None:
77
+ if is_private_ip(ip):
78
+ return None
28
79
 
29
- def resolve_domain_to_ip(domain):
30
80
  try:
31
- return socket.gethostbyname(domain)
32
- except socket.gaierror:
33
- print(f"Could not resolve domain: {domain}")
81
+ r = requests.get(
82
+ f"http://ip-api.com/json/{ip}?fields=status,lat,lon",
83
+ timeout=4,
84
+ )
85
+ data = r.json()
86
+ except Exception:
34
87
  return None
88
+
89
+ if data.get("status") != "success":
90
+ return None
91
+
92
+ return {
93
+ "latitude": data.get("lat"),
94
+ "longitude": data.get("lon"),
95
+ }