Interfaces
All network programming in P2PD starts with the network interface card. Usually your computer will have a ‘default’ interface that traffic is sent down based on various routing methods. Let’s start by loading this default interface and interacting with it. Starting the interface looks up all its addresses and enumerates the NAT of its associated router.
from p2pd import *
async def example():
# Start the default interface.
nic = await Interface()
# Load additional NAT details.
# Restrict, random port NAT assumed by default.
await nic.load_nat()
# Show the interface details.
print(nic)
if __name__ == '__main__':
async_test(example)
Interface.from_dict({
"name": "Intel(R) Wi-Fi 6 AX200 160MHz",
"nat": {
"type": 5,
"nat_info": "restrict port",
"delta": {
"type": 6,
"value": 0
},
"delta_info": "random delta (local port == rand port)"
},
"rp": {
"2": [
{
"af": 2,
"nic_ips": [
{
"ip": "192.168.21.21",
"cidr": 32,
"af": 2
}
],
"ext_ips": [
{
"ip": "1.3.3.7",
"cidr": 32,
"af": 2
}
]
}
],
"23": []
}
})
Repr shows a serializable dict representation of the interface after it’s been loaded. You can see a list of interfaces available on your machine by using the list_interfaces function. Interfaces may be virtual, contain loopback devices, and other adapters that aren’t directly useful for networking. Often we are only interested in the adapters that are usable for WAN or LAN networking.
from p2pd import *
async def example():
# Returns a list of Interface names.
if_names = await list_interfaces()
ifs = await load_interfaces(if_names)
print(ifs)
if __name__ == '__main__':
async_test(example)
Now you know how to lookup interfaces and start them. It’s time to learn about ‘routes.’
The addressing problem
Modern event loops make it easy to write high-performance networking code. The engineers of today are spoiled by such elegant features compared to the tools available in the early days. But there is still something very basic missing from the networking toolbox:
The ability to see your external addressing information
There are many cases where this information is needed. For example: imagine a server that listens on multiple IPs such that it is available on more than one external IP. The server may wish to know what external IPs are available to it in case it needs to refer a client to another server. The STUN protocol is the perfect instance where a client can request a connection back ‘from a different IP address’ in order to determine what type of NAT they have.
Hint
P2PD makes external addressing details available to the programmer. Such information avoids having to manually pass details to bind() to use a given external IP.
Routes to the rescue
P2PD solves the addressing problem by introducing mappings called ‘Routes’. A Route describes how interface-assigned addresses relate to external addresses. Each route is indexed by address family. Either IPv4 or IPv6.
Example 1 – IPv4 routes
NIC IPs:
192.168.0.20/32 (1 IP)
193.168.0.0/16 (65024 IPs)
7.7.7.7/32 (1 IP)
8.8.0.0/16 (65024 IPs)
EXT IPs:
1.3.3.7/32 (1 IP)
8.8.0.0/16 (65024 IPs)
---------------------------------------------------------------
Routes:
[...20, 193..., 7.7.7.7] -> [1.3.3.7]
[8.8.0.0] -> [8.8.0.0]
The software starts by grouping private IPs. It binds to the first and checks the external IP. The result is a new route with the external IP. If it finds a public IP for a NIC address it binds to the first IP in it’s range (range if it’s a block) and checks the external IP. If the IPs match it assumes the range is valid. If it matches the previous route it groups them as the same route.
Example: the software finds a block of NIC IPs ‘8.8.0.0/16’ with ‘public IPs.’ It binds to the first address and sees the external address matches. Seeing that it assumes this means the whole block is valid without checking every IP. This becomes a new route. This shows how some machines set their NIC IPs to their external addresses.
Example 2 – IPv6 routes
NIC IPS:
2020:DEED:BEEF::0000/128 (global scope) (1 IP)
2020:DEED:DEED::0000/64 (global scope) (a lot of IPs)
FE80:DEED:BEEF::0000/128 (link-local) (1 IP)
EXT IPS:
2020:DEED:BEEF::0000/128 (global scope) (1 IP)
2020:DEED:DEED::0000/64 (global scope) (a lot of IPs)
---------------------------------------------------------------
Routes:
[FE80:DEED:BEEF::0000/128] -> [2020:DEED:BEEF::0000/128]
[FE80:DEED:BEEF::0000/128] -> [2020:DEED:DEED::0000/64]
The algorithm for IPv6 routes is slightly different. All link-local addresses are copied to each route. While every global address ‘EXT’ forms a new route.
P2PD uses the EXT portion for IPv6 servers. While it uses the NIC portion for IPv4. It is assumed that all servers should be publicly reachable. Though this can be bypassed by specifying IPs directly for bind calls which is indeed what the P2PD REST server does.
If that sounds difficult Daemons make this easier.
Hint
There’s other ‘types’ of addresses in IPv6 though they’re not supported in P2PD for now. Just the equivalent of ‘private’ and ‘public’ addresses.
Using a route with a pipe
Connect to Google.com and get a response from it.
from p2pd import *
async def example():
# Load default interface.
nic = await Interface()
# Get a route to use for sockets.
# This will give you a copy of the first route for that address family.
# Routes belong to an interface and include a reference to it.
route = await nic.route(IP4).bind()
# Lookup Google.com's IP address -- specify a specific address family.
# Most websites support IPv4 but not always IPv6.
# Interface is needed to resolve some specialty edge-cases.
dest = ("8.8.8.8", 53)
# Now open a TCP connection to that the destination.
pipe = await pipe_open(TCP, dest, route)
# Send it a malformed HTTP request.
buf = b"Test\r\n\r\n"
await pipe.send(buf)
# Wait for any message response.
out = await pipe.recv(timeout=3)
print(out)
# Cleanup.
await pipe.close()
if __name__ == '__main__':
async_test(example)
Look closely at the route part. What this code is doing is it’s asking for the first route in the route pool for that address family. The route points to one or more external addresses (more if it’s a block) and ‘knowns’ how to setup the tuples for bind to use that external address. Once a route is bound it can be used in the familiar open_pipe call.
Hint
You can see from this example that P2PD supports duel-stack networking, multiple network interface cards, external addressing, DNS / IP / target parsing, and publish-subscribe. But there are many more useful features for network programming.
Route pools
Every interface with Internet access has at least one route in its route pool. If you don’t care about what route you’re using you can call the route function from an interface object. But through the RoutePool class every single possible external address can be used.
nic = await Interface()
x = nic.rp[IP4] # IP4 RoutePool
y = nic.rp[IP6] # IP6 RoutePool
print(len(x))
for r in x:
# A route to use a single external IP from the pool.
print(r)
The route pool is designed to make it easy to group multiple external addresses that may be themselves either single addresses or blocks of IPs into one object that can be indexed, counted, iterated, and used. In this way its easy to ‘grab an IP’ and use it.