Skip to content

Add support for IPv6/NAT64 device connection addresses#718

Merged
jasonacox merged 1 commit into
jasonacox:masterfrom
Kasoo:feature/ipv6-nat64-support
Jun 29, 2026
Merged

Add support for IPv6/NAT64 device connection addresses#718
jasonacox merged 1 commit into
jasonacox:masterfrom
Kasoo:feature/ipv6-nat64-support

Conversation

@Kasoo

@Kasoo Kasoo commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Enable tinytuya to connect to Tuya devices via IPv6 addresses, primarily to support NAT64 environments where an IPv6-only host needs to reach IPv4 Tuya devices via a NAT64 gateway.

The socket address family is now selected dynamically based on the device address string: if the address contains a colon it is treated as IPv6 (AF_INET6), otherwise as IPv4 (AF_INET). This covers plain IPv6, NAT64-translated addresses (e.g. 64:ff9b::192.168.1.175), and IPv4-mapped IPv6 addresses (e.g. ::ffff:192.168.1.1).

Note: Device discovery is still IPv4-only.

Enable tinytuya to connect to Tuya devices via IPv6 addresses,
primarily to support NAT64 environments where an IPv6-only host
needs to reach IPv4 Tuya devices via a NAT64 gateway.

The socket address family is now selected dynamically based on the
device address string: if the address contains a colon it is treated
as IPv6 (AF_INET6), otherwise as IPv4 (AF_INET). This covers plain
IPv6, NAT64-translated addresses (e.g. 64:ff9b::192.168.1.175),
and IPv4-mapped IPv6 addresses (e.g. ::ffff:192.168.1.1).

Note: Device discovery is still IPv4-only.

@jasonacox-sam jasonacox-sam left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this contribution! Clean, minimal approach — detecting IPv6 via : in the address string is reliable (IPv4 never contains colons) and the socket family selection covers plain IPv6, NAT64-translated, and IPv4-mapped addresses.

A couple of observations:

Looks good:

  • Change is correctly scoped to _get_socket() — the connection point — without touching discovery or protocol logic
  • Existing IPv4 behavior is unchanged (no :AF_INET)
  • socket.connect() with the right family handles the rest

Worth noting (not blocking):

  • Device discovery (scanner.py) is still IPv4-only as you mentioned. That's a reasonable scope limit for this PR — discovery uses UDP broadcast which is a different challenge on IPv6 (multicast ff02::1 etc.). Could be a follow-up if there's demand.
  • Hostnames containing colons would theoretically misroute, but in practice Tuya devices are always addressed by IP, not hostname — so this isn't a real concern.

Nice work — this fills a real gap for NAT64/IPv6-only environments. — Sam ⚙️

@jasonacox

Copy link
Copy Markdown
Owner

@jasonacox-sam what would this look like if we did add ipv6 support to the scanner?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator

@jasonacox — good question. Here's what it would take:

Three UDP listener sockets (lines ~1195, 1206, 1217) currently hardcode AF_INET:

  • v3.1 on port 6666, v3.3 on port 6667, app on port 7000
  • Would need dual-stack AF_INET6 sockets (with IPV6_V6ONLY=0) so they can catch device announcements arriving via NAT64 translation

Discovery broadcast (send_discovery_request, line ~233) currently uses IPv4 broadcast (SO_BROADCAST to 255.255.255.255 or subnet broadcast). IPv6 has no broadcast — it uses multicast:

  • Link-local all-nodes ff02::1 is the closest equivalent
  • The real challenge: Tuya devices are IPv4-only on the wire, so multicast discovery only helps if a NAT64 gateway translates the incoming device responses (which it will, since the source address gets translated)
  • Alternatively: if the NAT64 prefix is known (e.g. 64:ff9b::/96), synthesize IPv6 destinations from cloud-API-known device IPv4 addresses

Interface enumeration (get_ip_to_broadcast, line ~183) currently only pulls IPv4 broadcast addresses. The netifaces code at line 137 actually already has AF_INET6 commented out — uncomment and extend to build IPv6 multicast targets instead of broadcast addresses.

Force-scan TCP socket (line 332 in ForceScannedDevice.connect()) has the same AF_INET hardcode that PR #718 already fixes in XenonDevice._get_socket() — straightforward to apply the same family-detection pattern.

Summary of scope: The listener side is the easy part (dual-stack sockets). The hard part is discovery — you can't just "broadcast" to IPv6, you need either (a) multicast + hope NAT64 translates responses, or (b) know the NAT64 prefix and synthesize addresses. Neither is trivial.

If we wanted to do it incrementally, I'd suggest: (1) dual-stack listeners first, (2) same family-detection fix in force-scan, (3) multicast discovery as a follow-up if there's demand. — Sam ⚙️

@jasonacox

Copy link
Copy Markdown
Owner

Thanks @jasonacox-sam - I tend to agree, this is a heavy lift and I like the incremental approach. Can you do a hardware test of this PR as well as a test using the dual-stack listener?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator

@jasonacox — test run complete on the PR branch (f30100c) with no state-changing commands sent.

What I verified:

  • Live hardware: Kitchen Light responded successfully both via its normal IPv4 address and via an IPv4-mapped IPv6 address (::ffff:<LAN_IP>).
  • Socket-family instrumentation: on master, the mapped address still goes through AF_INET; on this PR branch, it switches to AF_INET6 exactly as intended.
  • Dual-stack listener check: a local AF_INET6 UDP listener with IPV6_V6ONLY=0 successfully received an IPv4 datagram as ::ffff:127.0.0.1, so the incremental scanner/listener approach we discussed is viable on this host.

One nuance: on this Linux box I couldn’t reproduce a functional failure on master with the mapped address alone — the OS is permissive there — but the PR does change the code path correctly, and the dual-stack listener behavior checked out locally too.

— Sam ⚙️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds IPv6-capable socket connection support for XenonDevice so tinytuya can connect to Tuya devices from IPv6-only/NAT64 environments by selecting the socket address family dynamically from the configured device address.

Changes:

  • Select AF_INET6 vs AF_INET when creating the TCP socket based on whether the device address string appears to be IPv6.
  • Enable connecting to literal IPv6 / NAT64 / IPv4-mapped IPv6 device addresses (discovery remains IPv4-only).
Comments suppressed due to low confidence (1)

tinytuya/core/XenonDevice.py:393

  • Creating an AF_INET6 socket here will break the subsequent connect((self.address, self.port)) call: IPv6 sockets expect a 4-tuple sockaddr (host, port, flowinfo, scopeid), so this will raise at runtime for IPv6/NAT64 addresses. Use socket.getaddrinfo() to obtain the correct sockaddr for the selected family (and create the socket from the returned af/socktype/proto), then connect using that sockaddr.
                family = socket.AF_INET6 if ':' in self.address else socket.AF_INET
                self.socket = socket.socket(family, socket.SOCK_STREAM)
                if self.socketNODELAY:
                    self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                self.socket.settimeout(self.connection_timeout)
                try:
                    retries = retries + 1
                    self.socket.connect((self.address, self.port))

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jasonacox

Copy link
Copy Markdown
Owner

tinytuya/core/XenonDevice.py:393
Creating an AF_INET6 socket here will break the subsequent connect((self.address, self.port)) call: IPv6 sockets expect a 4-tuple sockaddr (host, port, flowinfo, scopeid), so this will raise at runtime for IPv6/NAT64 addresses. Use socket.getaddrinfo() to obtain the correct sockaddr for the selected family (and create the socket from the returned af/socktype/proto), then connect using that sockaddr.

@jasonacox-sam Copilot provided this low-confidence feedback. Any concern with this?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator

@jasonacox — no concern here. Copilot's feedback is technically correct for C (struct sockaddr_in6 requires a 4-tuple), but Python's socket module accepts a 2-tuple for AF_INET6 connect calls and internally pads flowinfo and scopeid to 0.

I verified this directly: created an AF_INET6 socket and called connect((host, port)) with a 2-tuple — it reaches the network layer without any TypeError or address format error. The hardware test I ran earlier (Kitchen Light via IPv4-mapped IPv6 address) also exercised this exact code path successfully on the PR branch.

No changes needed for this PR. The 2-line fix is correct as-is. — Sam ⚙️

@jasonacox jasonacox merged commit 4e1e33b into jasonacox:master Jun 29, 2026
17 checks passed
@jasonacox

Copy link
Copy Markdown
Owner

Excellent, thanks @jasonacox-sam!

And thanks @Kasoo for the enhancement! 🙏 This is queued up for the next release.

@uzlonewolf

uzlonewolf commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

IMO it would be better to detect if 2 :'s are present as IPv6 uses a minimum of 2 (:: is the shortest IPv6 address possible) and since we're talking about address translation already I could see someone wanting to pass an address+port with 192.0.2.2:1234 (or [2001:db8::2]:1234). Might be a fun future enhancement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants