-
Notifications
You must be signed in to change notification settings - Fork 191
WIP: Auto-TLS support for py-libp2p #1072
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: main
Are you sure you want to change the base?
Conversation
examples/autotls/autotls.py
Outdated
| public_addrs = [f"/ip4/13.126.88.127/tcp/{port}/p2p/{host.get_id()}"] | ||
|
|
||
| server_id, bearer = http_peer_id_auth(host.get_private_key(), key_auth, public_addrs) |
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.
Auto-TLS will only work with public-routable multiaddrs, so here this ip 13.126.88.127 is from an aws-ec2 instance that I was using for testing this PR.
@lla-dane : Hi Abhinav. Fantastic progress on autotls module. Thank you so much for sharing the details. Appreciate it. Wish to ask if you found the fix in trio.py. Please also resolve the ci/cd issues whenever you get a chance. |
- Enhanced get_remote_address() in TrioTCPStream with address caching and defensive checks to handle socket state transitions gracefully - Fixed Ed25519PublicKey initialization to use from_bytes() method - Added proper type annotation for server_id: ID | None - Added None check for hostname before passing to ClientInitiatedHandshake - Removed unused variables (commented with explanations for future use) - Removed dead code (unused function calls with hardcoded port) - Removed debug print statements in favor of proper logging - Fixed code formatting, import ordering, and line length violations This resolves the get_remote_address() exception that was occurring when the Auto-TLS broker dials back into the node. Fixes issue reported in PR libp2p#1072 comments.
Fixes for Auto-TLS PR #1072This commit addresses the 🔧 Main Fix: Enhanced
|
Add examples.autotls to the examples.rst toctree to resolve the documentation build warning about the document not being included in any toctree.
Add the auto-generated examples.autotls.rst file to the repository so that ReadTheDocs can find it when building the documentation. This file is generated by sphinx-apidoc and is referenced in the examples.rst toctree.
| async def negotiate( | ||
| self, | ||
| communicator: IMultiselectCommunicator, | ||
| negotiate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT, | ||
| ) -> tuple[TProtocol | None, StreamHandlerFn | None]: | ||
| """ | ||
| Negotiate performs protocol selection. | ||
| :param stream: stream to negotiate on | ||
| :param negotiate_timeout: timeout for negotiation | ||
| :return: selected protocol name, handler function | ||
| :raise MultiselectError: raised when negotiation failed | ||
| """ | ||
| try: | ||
| with trio.fail_after(negotiate_timeout): | ||
| await self.handshake(communicator) | ||
|
|
||
| while True: | ||
| try: | ||
| print("\nNEGOTIATE LOOP") | ||
| command = await communicator.read() | ||
| print("COMMAND: ", command) | ||
| except MultiselectCommunicatorError as error: | ||
| print("ERROR IN NEGOTIATE READ") | ||
| raise MultiselectError() from error | ||
|
|
||
| if command == "ls": | ||
| supported_protocols = [ | ||
| p for p in self.handlers.keys() if p is not None | ||
| ] | ||
| response = "\n".join(supported_protocols) + "\n" | ||
|
|
||
| try: | ||
| await communicator.write(response) | ||
| except MultiselectCommunicatorError as error: | ||
| raise MultiselectError() from error | ||
|
|
||
| else: | ||
| protocol_to_check = None if not command else TProtocol(command) | ||
| if protocol_to_check in self.handlers: | ||
| try: | ||
| await communicator.write(command) | ||
| except MultiselectCommunicatorError as error: | ||
| raise MultiselectError() from error | ||
|
|
||
| return protocol_to_check, self.handlers[protocol_to_check] | ||
| try: | ||
| await communicator.write(PROTOCOL_NOT_FOUND_MSG) | ||
| print("PROTOCOL NOT IN HANDLERS: ", command) | ||
|
|
||
| except MultiselectCommunicatorError as error: | ||
| print("ERROR IN NEGOTIATE WRITE") | ||
| raise MultiselectError() from error | ||
|
|
||
| raise MultiselectError("Negotiation failed: no matching protocol") |
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.
Debugged further and found an issue happening here:
as we see the broker wrote tls/1.0.0 and we wrote back na as we did not had the handler for tls, so now after this, the loop should have continued, and the broker should try for another security option, but rather we got a read error.
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.
But this does not happen, when I dialed back to our python node, from a go-libp2p node.
Here the negotiation continued after this log
NEGOTIATE LOOP
COMMAND: /tls/1.0.0
PROTOCOL NOT IN HANDLERS: /tls/1.0.0
but the same thing does happen when the auto-tls broker dials in. I dont understand why this happens.
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.
@lla-dane I can't see the full images, can you please include the logs. with full commands, and output in text .
And a clear explanation to how to do the test and what you expect.
I'm confused sometimes I see echo and ping why ?
thanks
|
Yeah sure @acul71, I will explain everything properly. So in the autotls procedure, the autotls-broker has to dial in our node (which has to bee publicly accesible) and run identify protocol on our node, too see that our node is real or not. So presently when the autotls-broker is dialing in our node, there is some issue happening in the multiselect-stream protocol negotiation. LOGS: These are the first logs. There are basically to run the autotls-demo script. Here we got dialed in here in this part |
|
Since the p2p-forge autotls-broker repo: https://github.com/ipshipyard/p2p-forge, uses go-libp2p, I dialed in our node from a go-libp2p node to see what happens during the multistream-select protocol neogtiation. DIALER: LISTENER: for just debugging purpose, I dialed to our py-libp2p node from the echo example of go-libp2p. I just needed to see how the multistream-select protocol negotiation goes. |
|
@acul71: For testing, I have DM'd you the ec2 instance keys and how to connect to the instance on discord. There you can simply run the |
|
Hello @lla-dane @seetadev ############ Suspecting it could be a transport-select negotiation issue, I've verified that py-libp2p's transport-select negotiation works correctly with the main branch. The broker negotiation issue seems not a py-libp2p transport-select bug, but rather a broker-specific issue in the HTTP handler context. Evidence: Broker Simulation TestI created a Go dialer that exactly replicates the broker's Test ResultsBroker Simulation CodeThe following Go program exactly mimics the broker's connection behavior: package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/net/swarm"
ma "github.com/multiformats/go-multiaddr"
)
// This mimics what the broker does: creates a libp2p host with default security
// (TLS + Noise) and tries to connect to another peer.
func main() {
if len(os.Args) < 2 {
log.Fatal("Usage: dialer <multiaddr>")
}
targetAddr := os.Args[1]
fmt.Printf("Creating libp2p host with default security (TLS + Noise)...\n")
// Create host EXACTLY like the broker does
h, err := libp2p.New(
libp2p.NoListenAddrs,
libp2p.DisableRelay(),
libp2p.WithDialTimeout(10*time.Second),
libp2p.SwarmOpts(swarm.WithDialTimeoutLocal(10*time.Second)),
)
if err != nil {
log.Fatalf("Failed to create host: %v", err)
}
defer h.Close()
fmt.Printf("Host created with ID: %s\n", h.ID())
fmt.Printf("Dialing target: %s\n", targetAddr)
// Parse the multiaddr
maddr, err := ma.NewMultiaddr(targetAddr)
if err != nil {
log.Fatalf("Invalid multiaddr: %v", err)
}
// Extract peer info
info, err := peer.AddrInfoFromP2pAddr(maddr)
if err != nil {
log.Fatalf("Failed to extract peer info: %v", err)
}
fmt.Printf("Target peer ID: %s\n", info.ID)
fmt.Printf("Target addresses: %v\n", info.Addrs)
// Try to connect with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
startTime := time.Now()
fmt.Printf("Connecting...\n")
err = h.Connect(ctx, *info)
duration := time.Since(startTime)
if err != nil {
fmt.Printf("FAILED to connect after %v: %v\n", duration, err)
} else {
fmt.Printf("SUCCESS! Connected in %v\n", duration)
// Check what security protocol was used
conns := h.Network().ConnsToPeer(info.ID)
for _, conn := range conns {
fmt.Printf("Connection security: %s\n", conn.ConnState().Security)
}
}
}Python Listener CodeThe Python listener used for testing (main branch, no fixes): #!/usr/bin/env python3
"""
Simple Python libp2p listener for incremental fix testing.
This will be used to test each fix incrementally with the Go dialer.
"""
import logging
import sys
import trio
import multiaddr
import socket
# Enable basic logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
from libp2p import new_host
from libp2p.crypto.secp256k1 import create_new_key_pair
from libp2p.custom_types import TProtocol
def find_free_port():
"""Find a free port for listening."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
s.listen(1)
port = s.getsockname()[1]
return port
PROTOCOL_ID = TProtocol("/echo/1.0.0")
async def echo_handler(stream):
"""Echo handler - reads and echoes back data."""
print(f"[ECHO] Got stream! Protocol: {stream.get_protocol()}", flush=True)
try:
data = await stream.read(1024)
print(f"[ECHO] Received: {data!r}", flush=True)
await stream.write(data)
print(f"[ECHO] Echoed back", flush=True)
except Exception as e:
print(f"[ECHO] Error: {e}", flush=True)
finally:
await stream.close()
async def main():
port = find_free_port()
print(f"[MAIN] Creating libp2p host on port {port}...", flush=True)
# Create a deterministic key for testing
key_pair = create_new_key_pair()
host = new_host(key_pair=key_pair)
listen_addr = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{port}")]
async with host.run(listen_addrs=listen_addr):
# Set up echo handler
host.set_stream_handler(PROTOCOL_ID, echo_handler)
peer_id = host.get_id().to_string()
full_addr = f"/ip4/127.0.0.1/tcp/{port}/p2p/{peer_id}"
print(f"\n{'='*60}", flush=True)
print(f"[MAIN] Python libp2p host started!", flush=True)
print(f"[MAIN] Peer ID: {peer_id}", flush=True)
print(f"[MAIN] Full address: {full_addr}", flush=True)
print(f"\n[MAIN] To test with Go dialer, run:", flush=True)
print(f" /tmp/test_broker_go/dialer_test \"{full_addr}\"", flush=True)
print(f"{'='*60}\n", flush=True)
print("[MAIN] Waiting for connections (Ctrl+C to stop)...", flush=True)
# Wait forever
try:
await trio.sleep_forever()
except KeyboardInterrupt:
print("\n[MAIN] Shutting down...", flush=True)
if __name__ == "__main__":
trio.run(main)Test ExecutionPython listener (main branch, no fixes): cd /tmp/py-libp2p && source venv/bin/activate
python3 test_incremental_fixes.py
# Output: Listening on /ip4/127.0.0.1/tcp/54027/p2p/16Uiu2HAm...Go dialer (broker simulation): /tmp/test_broker_go/dialer_test "/ip4/127.0.0.1/tcp/54027/p2p/16Uiu2HAm..."Result: What This Proves
Comparison: Broker Replica vs Actual Broker
Key difference: Broker replica runs as standalone program, actual broker runs inside HTTP handler context. |





Aims to resolve #555
Auto-TLS support for py-libp2p in reference with the auto-tls client spec.
References:
https://github.com/libp2p/specs/blob/master/tls/autotls-client.md
https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md
https://blog.libp2p.io/autotls/