@@ -577,7 +577,6 @@ impl FromStr for ChannelAddr {
577
577
type Err = anyhow:: Error ;
578
578
579
579
fn from_str ( addr : & str ) -> Result < Self , Self :: Err > {
580
- // "!" is the legacy delimiter; ":" is preferred
581
580
match addr. split_once ( '!' ) . or_else ( || addr. split_once ( ':' ) ) {
582
581
Some ( ( "local" , rest) ) => rest
583
582
. parse :: < u64 > ( )
@@ -596,6 +595,102 @@ impl FromStr for ChannelAddr {
596
595
}
597
596
}
598
597
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
+
599
694
/// Universal channel transmitter.
600
695
#[ derive( Debug ) ]
601
696
pub struct ChannelTx < M : RemoteMessage > {
@@ -832,6 +927,78 @@ mod tests {
832
927
}
833
928
}
834
929
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
+
835
1002
#[ tokio:: test]
836
1003
async fn test_multiple_connections ( ) {
837
1004
for addr in ChannelTransport :: all ( ) . map ( ChannelAddr :: any) {
0 commit comments