Skip to content

extmod/asyncio/stream.py: Add ipv6 support to start_server(). #17311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions extmod/asyncio/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,11 @@ async def start_server(cb, host, port, backlog=5, ssl=None):
import socket

# Create and bind server socket.
host = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket()
addr_info = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be possible to add the family argument to start_server() and pass it through to getaddrinfo() here. Then you could force it to use IPv6 (or IPv4). But maybe that's a separate PR?

s = socket.socket(addr_info[0]) # Use address family from getaddrinfo
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(host[-1])
s.bind(addr_info[-1])
s.listen(backlog)

# Create and return server object and task.
Expand Down
72 changes: 72 additions & 0 deletions tests/multi_net/asyncio_tcp_server_client_ipv6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Test asyncio TCP server and client using start_server() and open_connection() with IPv6

try:
import asyncio
import socket
except ImportError:
print("SKIP")
raise SystemExit

try:
# Check if IPv6 is supported
socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This detection won't work. All ports based on bare-metal lwIP will let this pass even if they don't have IPv6 enabled.

I don't know if it's possible to detect IPv6??

except (AttributeError, OSError):
print("SKIP")
raise SystemExit

PORT = 8001 # Different from other tests to avoid conflicts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to use a different port, 8000 should be fine.



async def handle_connection(reader, writer):
# Test that peername exists
peer = writer.get_extra_info("peername")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The peer variable is unused. I don't think you need to call this function at all.

print("peer connected")

data = await reader.read(100)
print("echo:", data)
writer.write(data)
await writer.drain()

print("close")
writer.close()
await writer.wait_closed()

print("done")
ev.set()


async def tcp_server_ipv6():
global ev
ev = asyncio.Event()

# Start server with IPv6 address
server = await asyncio.start_server(handle_connection, "::", PORT)
print("ipv6 server running")
multitest.next()

async with server:
await asyncio.wait_for(ev.wait(), 10)


async def tcp_client_ipv6(message):
# Connect to the IPv6 server
reader, writer = await asyncio.open_connection(IPV6, PORT)
print("write:", message)
writer.write(message)
await writer.drain()

data = await reader.read(100)
print("read:", data)
assert data == message, "Data mismatch"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this assert, the .exp will test the data is correct.



def instance0():
# Get the IPv6 address using the new parameter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not needed, it won't be a new parameter after the PR is merged.

ipv6 = multitest.get_network_ip(ipv6=True)
multitest.globals(IPV6=ipv6)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use just a single line, like the other tests:

multitest.globals(IP=multitest.get_network_ip(ipv6=True))

asyncio.run(tcp_server_ipv6())


def instance1():
multitest.next()
asyncio.run(tcp_client_ipv6(b"ipv6 client data"))
79 changes: 79 additions & 0 deletions tests/net_hosted/asyncio_start_server_ipv6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Test asyncio.start_server() with IPv6 address

try:
import asyncio
import socket
except ImportError:
print("SKIP")
raise SystemExit

try:
# Check if IPv6 is supported
socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, this detection won't work.

except (AttributeError, OSError):
print("SKIP")
raise SystemExit

PORT = 8000


async def handle_connection(reader, writer):
data = await reader.read(100)
print("echo:", data)
writer.write(data)
await writer.drain()

print("close")
writer.close()
await writer.wait_closed()

print("done")
global server_done
server_done = True


async def test_ipv6_server():
global server_done
server_done = False

# Start server with IPv6 address
print("create ipv6 server")
server = await asyncio.start_server(handle_connection, "::", PORT)
print("server running")

try:
# Connect with IPv6 client
print("connect to ipv6 server")
reader, writer = await asyncio.open_connection("::", PORT)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this to work requires loopback mode in the TCP/IP driver. That's fine on unix, but it'll fail on most bare-metal ports.


# Send test data
test_msg = b"ipv6 test data"
print("write:", test_msg)
writer.write(test_msg)
await writer.drain()

# Read response
data = await reader.read(100)
print("read:", data)
assert data == test_msg, "Data mismatch"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert not needed.


# Close client connection
print("close client")
writer.close()
await writer.wait_closed()

# Wait for server to complete handling
while not server_done:
await asyncio.sleep(0.1)

finally:
# Ensure server is closed
print("close server")
server.close()
await server.wait_closed()

print("test passed")


# Run the test
asyncio.run(test_ipv6_server())
95 changes: 80 additions & 15 deletions tests/run-multitests.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,59 @@ def globals(**gs):
print("SET {{}} = {{!r}}".format(g, gs[g]))
multitest.flush()
@staticmethod
def get_network_ip():
def _get_ip_from_ifconfig(_nic, ipv6=False):
# Helper to get IP address from an interface object using appropriate format
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn't add much, and I'm confused what "appropriate format" means?

addr_type = 'addr6' if ipv6 else 'addr4'

# First try newer format with addr type parameter
try:
ip = nic.ifconfig()[0]
ip = _nic.ifconfig(addr_type)
if isinstance(ip, tuple) and len(ip) > 0:
return ip[0]
return ip
except:
try:
import network
if hasattr(network, "WLAN"):
ip = network.WLAN().ifconfig()[0]
else:
ip = network.LAN().ifconfig()[0]
except:
ip = HOST_IP
return ip
# Fallback to legacy format, but only for IPv4
if not ipv6:
try:
return _nic.ifconfig()[0] # Legacy format
except:
pass
return None
@staticmethod
def get_network_ip(ipv6=False):
# Try with direct nic object if available
try:
if 'nic' in globals():
ip = multitest._get_ip_from_ifconfig(nic, ipv6)
if ip:
return ip
except:
pass

# Find active network interface
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code below is very hard to understand. Why so many try/except's?

import network
for attr_name in dir(network):
if attr_name.startswith('__'):
continue
try:
net_class = getattr(network, attr_name)
if hasattr(net_class, 'active'):
try:
net_obj = net_class()
if net_obj.active() and hasattr(net_obj, 'ifconfig'):
ip = multitest._get_ip_from_ifconfig(net_obj, ipv6)
if ip:
return ip
except:
pass
except:
pass
except:
pass

# Fallback to host IP
return HOST_IP6 if ipv6 else HOST_IP
@staticmethod
def expect_reboot(resume, delay_ms=0):
print("WAIT_FOR_REBOOT", resume, delay_ms)
Expand Down Expand Up @@ -130,6 +170,24 @@ def get_host_ip(_ip_cache=[]):
return _ip_cache[0]


def get_host_ipv6(_ipv6_cache=[]):
if not _ipv6_cache:
try:
import socket

# Try to find an IPv6 address by creating an IPv6 socket
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
# Connect to IPv6 Google DNS server to get our IPv6 address
s.connect(("2001:4860:4860::8888", 80))
addr = s.getsockname()[0]
s.close()
_ipv6_cache.append(addr)
except:
# Fallback to localhost if unable to determine IPv6 address
_ipv6_cache.append("::1")
return _ipv6_cache[0]


class PyInstance:
def __init__(self):
pass
Expand Down Expand Up @@ -330,12 +388,19 @@ def run_test_on_instances(test_file, num_instances, instances):
output = [[] for _ in range(num_instances)]
output_metrics = []

# If the test calls get_network_ip() then inject HOST_IP so that devices can know
# the IP address of the host. Do this lazily to not require a TCP/IP connection
# on the host if it's not needed.
# If the test calls get_network_ip() or get_network_ipv6() then inject HOST_IP and HOST_IP6
# so that devices can know the IP addresses of the host. Do this lazily to not require
# a TCP/IP connection on the host if it's not needed.
with open(test_file, "rb") as f:
if b"get_network_ip" in f.read():
file_content = f.read()
if b"get_network_ip" in file_content:
injected_globals += "HOST_IP = '" + get_host_ip() + "'\n"
# Also include IPv6 host IP if we can determine it
try:
host_ipv6 = get_host_ipv6()
injected_globals += "HOST_IP6 = '" + host_ipv6 + "'\n"
except:
injected_globals += "HOST_IP6 = '::1'\n" # Default to localhost

if cmd_args.trace_output:
print("TRACE {}:".format("|".join(str(i) for i in instances)))
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy