@@ -577,7 +577,6 @@ impl FromStr for ChannelAddr {
577577 type Err = anyhow:: Error ;
578578
579579 fn from_str ( addr : & str ) -> Result < Self , Self :: Err > {
580- // "!" is the legacy delimiter; ":" is preferred
581580 match addr. split_once ( '!' ) . or_else ( || addr. split_once ( ':' ) ) {
582581 Some ( ( "local" , rest) ) => rest
583582 . parse :: < u64 > ( )
@@ -596,6 +595,102 @@ impl FromStr for ChannelAddr {
596595 }
597596}
598597
598+ impl ChannelAddr {
599+ /// Parse ZMQ-style URL format: scheme://address
600+ /// Supports:
601+ /// - tcp://hostname:port or tcp://*:port (wildcard binding)
602+ /// - inproc://endpoint-name (equivalent to local)
603+ /// - ipc://path (equivalent to unix)
604+ /// - metatls://hostname:port or metatls://*:port
605+ pub fn from_zmq_url ( address : & str ) -> Result < Self , anyhow:: Error > {
606+ // Try ZMQ-style URL format first (scheme://...)
607+ let ( scheme, address) = address. split_once ( "://" ) . ok_or_else ( || {
608+ anyhow:: anyhow!( "address must be in url form scheme://endppoint {}" , address)
609+ } ) ?;
610+
611+ match scheme {
612+ "tcp" => {
613+ let ( host, port) = Self :: split_host_port ( address) ?;
614+
615+ if host == "*" {
616+ // Wildcard binding - use IPv6 unspecified address
617+ Ok ( Self :: Tcp ( SocketAddr :: new ( "::" . parse ( ) . unwrap ( ) , port) ) )
618+ } else {
619+ // Resolve hostname to IP address for proper SocketAddr creation
620+ let socket_addr = Self :: resolve_hostname_to_socket_addr ( host, port) ?;
621+ Ok ( Self :: Tcp ( socket_addr) )
622+ }
623+ }
624+ "inproc" => {
625+ // inproc://port -> local:port
626+ // Port must be a valid u64 number
627+ let port = address. parse :: < u64 > ( ) . map_err ( |_| {
628+ anyhow:: anyhow!( "inproc endpoint must be a valid port number: {}" , address)
629+ } ) ?;
630+ Ok ( Self :: Local ( port) )
631+ }
632+ "ipc" => {
633+ // ipc://path -> unix:path
634+ Ok ( Self :: Unix ( net:: unix:: SocketAddr :: from_str ( address) ?) )
635+ }
636+ "metatls" => {
637+ let ( host, port) = Self :: split_host_port ( address) ?;
638+
639+ if host == "*" {
640+ // Wildcard binding - use IPv6 unspecified address directly without hostname resolution
641+ Ok ( Self :: MetaTls ( MetaTlsAddr :: Host {
642+ hostname : std:: net:: Ipv6Addr :: UNSPECIFIED . to_string ( ) ,
643+ port,
644+ } ) )
645+ } else {
646+ Ok ( Self :: MetaTls ( MetaTlsAddr :: Host {
647+ hostname : host. to_string ( ) ,
648+ port,
649+ } ) )
650+ }
651+ }
652+ scheme => Err ( anyhow:: anyhow!( "unsupported ZMQ scheme: {}" , scheme) ) ,
653+ }
654+ }
655+
656+ /// Split host:port string, supporting IPv6 addresses
657+ fn split_host_port ( address : & str ) -> Result < ( & str , u16 ) , anyhow:: Error > {
658+ if let Some ( ( host, port_str) ) = address. rsplit_once ( ':' ) {
659+ let port: u16 = port_str
660+ . parse ( )
661+ . map_err ( |_| anyhow:: anyhow!( "invalid port: {}" , port_str) ) ?;
662+ Ok ( ( host, port) )
663+ } else {
664+ Err ( anyhow:: anyhow!( "invalid address format: {}" , address) )
665+ }
666+ }
667+
668+ /// Resolve hostname to SocketAddr, handling both IP addresses and hostnames
669+ fn resolve_hostname_to_socket_addr ( host : & str , port : u16 ) -> Result < SocketAddr , anyhow:: Error > {
670+ // Handle IPv6 addresses in brackets by stripping the brackets
671+ let host_clean = if host. starts_with ( '[' ) && host. ends_with ( ']' ) {
672+ & host[ 1 ..host. len ( ) - 1 ]
673+ } else {
674+ host
675+ } ;
676+
677+ // First try to parse as an IP address directly
678+ if let Ok ( ip_addr) = host_clean. parse :: < IpAddr > ( ) {
679+ return Ok ( SocketAddr :: new ( ip_addr, port) ) ;
680+ }
681+
682+ // If not an IP, try hostname resolution
683+ use std:: net:: ToSocketAddrs ;
684+ let mut addrs = ( host_clean, port)
685+ . to_socket_addrs ( )
686+ . map_err ( |e| anyhow:: anyhow!( "failed to resolve hostname '{}': {}" , host_clean, e) ) ?;
687+
688+ addrs
689+ . next ( )
690+ . ok_or_else ( || anyhow:: anyhow!( "no addresses found for hostname '{}'" , host_clean) )
691+ }
692+ }
693+
599694/// Universal channel transmitter.
600695#[ derive( Debug ) ]
601696pub struct ChannelTx < M : RemoteMessage > {
@@ -832,6 +927,78 @@ mod tests {
832927 }
833928 }
834929
930+ #[ test]
931+ fn test_zmq_style_channel_addr ( ) {
932+ // Test TCP addresses
933+ assert_eq ! (
934+ ChannelAddr :: from_zmq_url( "tcp://127.0.0.1:8080" ) . unwrap( ) ,
935+ ChannelAddr :: Tcp ( "127.0.0.1:8080" . parse( ) . unwrap( ) )
936+ ) ;
937+
938+ // Test TCP wildcard binding
939+ assert_eq ! (
940+ ChannelAddr :: from_zmq_url( "tcp://*:5555" ) . unwrap( ) ,
941+ ChannelAddr :: Tcp ( "[::]:5555" . parse( ) . unwrap( ) )
942+ ) ;
943+
944+ // Test inproc (maps to local with numeric endpoint)
945+ assert_eq ! (
946+ ChannelAddr :: from_zmq_url( "inproc://12345" ) . unwrap( ) ,
947+ ChannelAddr :: Local ( 12345 )
948+ ) ;
949+
950+ // Test ipc (maps to unix)
951+ assert_eq ! (
952+ ChannelAddr :: from_zmq_url( "ipc:///tmp/my-socket" ) . unwrap( ) ,
953+ ChannelAddr :: Unix ( unix:: SocketAddr :: from_pathname( "/tmp/my-socket" ) . unwrap( ) )
954+ ) ;
955+
956+ // Test metatls with hostname
957+ assert_eq ! (
958+ ChannelAddr :: from_zmq_url( "metatls://example.com:443" ) . unwrap( ) ,
959+ ChannelAddr :: MetaTls ( MetaTlsAddr :: Host {
960+ hostname: "example.com" . to_string( ) ,
961+ port: 443
962+ } )
963+ ) ;
964+
965+ // Test metatls with IP address (should be normalized)
966+ assert_eq ! (
967+ ChannelAddr :: from_zmq_url( "metatls://192.168.1.1:443" ) . unwrap( ) ,
968+ ChannelAddr :: MetaTls ( MetaTlsAddr :: Host {
969+ hostname: "192.168.1.1" . to_string( ) ,
970+ port: 443
971+ } )
972+ ) ;
973+
974+ // Test metatls with wildcard (should use IPv6 unspecified address)
975+ assert_eq ! (
976+ ChannelAddr :: from_zmq_url( "metatls://*:8443" ) . unwrap( ) ,
977+ ChannelAddr :: MetaTls ( MetaTlsAddr :: Host {
978+ hostname: "::" . to_string( ) ,
979+ port: 8443
980+ } )
981+ ) ;
982+
983+ // Test TCP hostname resolution (should resolve hostname to IP)
984+ // Note: This test may fail in environments without proper DNS resolution
985+ // We test that it at least doesn't fail to parse
986+ let tcp_hostname_result = ChannelAddr :: from_zmq_url ( "tcp://localhost:8080" ) ;
987+ assert ! ( tcp_hostname_result. is_ok( ) ) ;
988+
989+ // Test IPv6 address
990+ assert_eq ! (
991+ ChannelAddr :: from_zmq_url( "tcp://[::1]:1234" ) . unwrap( ) ,
992+ ChannelAddr :: Tcp ( "[::1]:1234" . parse( ) . unwrap( ) )
993+ ) ;
994+
995+ // Test error cases
996+ assert ! ( ChannelAddr :: from_zmq_url( "invalid://scheme" ) . is_err( ) ) ;
997+ assert ! ( ChannelAddr :: from_zmq_url( "tcp://invalid-port" ) . is_err( ) ) ;
998+ assert ! ( ChannelAddr :: from_zmq_url( "metatls://no-port" ) . is_err( ) ) ;
999+ assert ! ( ChannelAddr :: from_zmq_url( "inproc://not-a-number" ) . is_err( ) ) ;
1000+ }
1001+
8351002 #[ tokio:: test]
8361003 async fn test_multiple_connections ( ) {
8371004 for addr in ChannelTransport :: all ( ) . map ( ChannelAddr :: any) {
0 commit comments