Skip to content

Instantly share code, notes, and snippets.

@zed
Created April 4, 2020 16:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zed/939944cdc7c2f6a0eaab6dfd7eb5a88d to your computer and use it in GitHub Desktop.
Save zed/939944cdc7c2f6a0eaab6dfd7eb5a88d to your computer and use it in GitHub Desktop.
Get external/public ip using DNS, HTTP, STUN protocols
#!/usr/bin/env python3
"""Get external ip using DNS, HTTP, STUN protocols.
Print the first result (the fastest).
Usage:
$ python3 -m external_ip [--quiet]
Optional dependencies:
$ python3 -m pip install aidns pynat pystun3
"""
import asyncio
import functools
import ipaddress
import logging
import sys
import time
import urllib.request
try:
import aiodns # pip install aiodns
except ImportError:
aiodns = None
try:
import pynat # pip install pynat
except ImportError:
pynat = None
try:
import stun # pip install pystun3
except ImportError:
stun = None
logger = logging.getLogger(__name__)
def logged_duration(coro, timer=time.monotonic):
@functools.wraps(coro)
async def wrapper(*args, **kwargs):
start = timer()
try:
return await coro(*args, **kwargs)
finally:
logger.info(
"%5.2fms in %s", 1000 * (timer() - start), coro.__name__,
)
return wrapper
if aiodns:
@logged_duration
async def external_ipv4():
"""Return public IPv4 address."""
# OpenDNS name servers resolve magic domain to your external ip address
return await _external_ip_via_opendns(
nameservers=["208.67.222.222", "208.67.220.220"], query_type="A",
)
@logged_duration
async def external_ipv6():
"""Return public IPv6 address."""
return await _external_ip_via_opendns(
nameservers=["2620:119:35::35", "2620:119:53::53"],
query_type="AAAA",
)
async def _external_ip_via_opendns(*, nameservers, query_type):
"""OpenDNS *nameservers* resolve magic domain to your external ip address."""
resolver = aiodns.DNSResolver()
resolver.nameservers = nameservers
magic_domain = "myip.opendns.com"
try:
host = (await resolver.query(magic_domain, query_type))[0].host
return ipaddress.ip_address(host)
finally:
resolver.cancel()
@logged_duration
async def external_ip_via_ifconfig():
"""Return public ip via https://ifconfig.me/ip"""
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, _external_ip_via_ifconfig_blocking)
def _external_ip_via_ifconfig_blocking():
with urllib.request.urlopen("https://ifconfig.me/ip", timeout=1) as r:
return r.read(1024).decode("ascii")
if pynat:
@logged_duration
async def external_ip_via_pynat():
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, _external_ip_via_pynat_blocking)
def _external_ip_via_pynat_blocking():
return pynat.get_ip_info()[1]
if stun:
@logged_duration
async def external_ip_via_pystun3():
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None, _external_ip_via_pystun3_blocking
)
def _external_ip_via_pystun3_blocking():
return stun.get_ip_info()[1]
@logged_duration
async def external_ip():
"""Return public IP address."""
# run all external_ip.*() corotines, return the first available result
for f in asyncio.as_completed(
[
coro()
for name, coro in globals().items()
if name.startswith("external_ip") and name != "external_ip"
]
):
try:
return await f
except Exception as e:
last_error = e
raise last_error
async def main():
level = logging.WARNING if "--quiet" in sys.argv else logging.INFO
logging.basicConfig(
level=level, format="%(asctime)s %(message)s",
)
print(await external_ip())
if __name__ == "__main__":
asyncio.run(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment