A capture full of IP addresses is a list of numbers until you know where each one actually sits in the world. This article turns that list into a map, and then into a ranked table of who is talking to your network the most.
Analogy: The STD code
An old landline number carried its city in its STD code: a number beginning 011 was in Delhi, 022 in Mumbai, 079 in Ahmedabad. You could place a caller on the map before they said a word. A GeoIP lookup does the same for an IP address, mapping it to an approximate country and city based on how address ranges are allocated to providers worldwide. It turns a bare list of attacking addresses into a map of where an attack is coming from.
Tool in focus: PyGeoIP and its modern successor
The original syllabus names pygeoip with MaxMind's legacy GeoLiteCity.dat database. MaxMind has since retired that format in favour of GeoLite2, read by the newer geoip2 package, so a script written today should prefer geoip2 while the idea and most of the surrounding code stay the same. A GeoLite2 database is free to download with a MaxMind account and is refreshed periodically as allocations change.
geolocate_ip.py (current geoip2 package, GeoLite2 database)
import geoip2.database
reader = geoip2.database.Reader("GeoLite2-City.mmdb")
def locate(ip_address):
try:
r = reader.city(ip_address)
return {"country": r.country.name, "city": r.city.name,
"lat": r.location.latitude, "lon": r.location.longitude}
except geoip2.errors.AddressNotFoundError:
return None
info = locate("8.8.8.8")
print(f"{info['city']}, {info['country']}" if info else "Not found")
Caution: This one needs a database file this course cannot ship
geoip2.database.Reader opens a local .mmdb file, which is several megabytes and updated regularly, so it is never bundled with a course. The lab exercise at the end of this unit walks through getting a free one from MaxMind. The function's shape is exactly what you will use once you have it: reader.city() returns a rich object on a match, and raises geoip2.errors.AddressNotFoundError on a miss, which locate() turns into a plain None the rest of a script can check with a simple if.
Turning a capture into a ranked, located list
Correlating every unique source address in a capture with a location turns a list of numbers into intelligence, and the busiest sources are usually the first question an analyst asks of an unfamiliar capture.
correlate_sources.py
# correlate_sources.py
from collections import Counter
import dpkt, socket
from open_capture import read_packets
source_counts = Counter()
for ts, raw in read_packets("capture.pcap"):
try:
ip = dpkt.ethernet.Ethernet(raw).data
source_counts[socket.inet_ntoa(ip.src)] += 1
except AttributeError:
continue
for ip_address, count in source_counts.most_common(10):
info = locate(ip_address)
where = f"{info['city']}, {info['country']}" if info else "unknown"
print(f"{ip_address} ({count} packets) - {where}")
Run without a GeoLite2 database attached, the counting half alone, against a lab capture built for this unit, already shows the shape an analyst looks for:
Sample output (counts only; locations need a GeoLite2 database, see above)
185.220.101.44 (600 packets)
45.83.64.12 (600 packets)
103.145.13.9 (600 packets)
10.0.0.9 (40 packets)
10.0.0.20 (5 packets)
Three foreign addresses sending six hundred packets each, against the lab host's handful, are not customers. A capture that looks like this is a distributed attack, and once a GeoLite2 database is attached, the geography would show whether those three sources are spread across the world or clustered in one place, which is the next two articles' subject.
Python concept: collections.Counter
collections.Counter is a dictionary built for tallying. Each time you add a key it increments that key's count, and .most_common(10) returns the ten highest tallies, already sorted. It turns "which source sent the most packets" into two lines of code, a question you will ask constantly in the denial-of-service work ahead.
What a GeoIP lookup can and cannot tell you
It can
Place an address within a country, usually a city, with reasonable confidence for most ranges, based on how address blocks are publicly allocated to ISPs and organisations.
It cannot
Identify a person, confirm an address was not spoofed, or guarantee precision down to a street. A VPN, a proxy, or a compromised relay machine all show the location of that intermediate point, not necessarily the attacker sitting behind it.
Check your understanding
Question 1: Explain the STD-code analogy for GeoIP, and state one thing a GeoIP lookup can and cannot tell you about an attacker.
An old landline's area code placed a caller's city before they said a word, the same way an IP address's allocated range places it in an approximate location before any other evidence is read. It can place an address within a country and usually a city with reasonable confidence; it cannot identify the actual person behind the address or confirm the traffic was not routed through a VPN or proxy that hides the real source.
Question 2: What does Counter.most_common(10) return, and why is it useful on a capture with thousands of packets?
It returns the ten keys with the highest tallies, already sorted from most to least frequent. On a capture with thousands of packets, it is the fastest way to answer "who is sending the most traffic" without writing any sorting logic yourself.
Question 3: In correlate_sources.py, what does the except AttributeError clause actually catch?
A non-IP frame, such as ARP, whose data does not have a .src attribute in the shape the code expects. Accessing .src on something that is not an IP packet raises an AttributeError, and catching it lets the loop skip that one frame and continue with the rest of the capture.
Key takeaway: Key takeaway
A GeoIP lookup turns a bare address into an approximate place, and Counter turns a capture into a ranked list of who is talking the most. Together they are usually the first two questions an analyst asks of an unfamiliar capture, well before reading a single packet's payload.

