-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")) |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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 | ||
|
@@ -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))) | ||
|
There was a problem hiding this comment.
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 tostart_server()
and pass it through togetaddrinfo()
here. Then you could force it to use IPv6 (or IPv4). But maybe that's a separate PR?