@@ -818,9 +818,11 @@ fn validate_no_supervisor_profile(profile: &Sandbox, source: &str) -> Result<()>
818818
819819/// Render a parsed `NetRule` back into a `--net-allow` / `--net-deny` spec
820820/// string, so a profile loaded via `--profile-file` round-trips through the
821- /// builder. Allow and deny share one grammar: bare TCP, explicit
822- /// `udp://`/`icmp://`, IPv6 bracketed only when a port follows, and the
823- /// all-ports case drops the redundant `:*`.
821+ /// builder. Allow and deny share one grammar. The scheme is always
822+ /// rendered: a scheme-less spec parses as a TCP + UDP pair, so a
823+ /// single-protocol rule must carry its scheme to round-trip exactly.
824+ /// IPv6 is bracketed only when a port follows, and the all-ports case
825+ /// drops the redundant `:*`.
824826fn format_net_rule ( rule : & sandlock_core:: sandbox:: NetRule ) -> String {
825827 use sandlock_core:: sandbox:: { NetTarget , Protocol } ;
826828 let target = match & rule. target {
@@ -839,7 +841,7 @@ fn format_net_rule(rule: &sandlock_core::sandbox::NetRule) -> String {
839841 match rule. protocol {
840842 Protocol :: Icmp => format ! ( "icmp://{}" , target) ,
841843 proto => {
842- let scheme = if matches ! ( proto, Protocol :: Udp ) { "udp://" } else { "" } ;
844+ let scheme = if matches ! ( proto, Protocol :: Udp ) { "udp://" } else { "tcp:// " } ;
843845 if rule. all_ports {
844846 format ! ( "{}{}" , scheme, target)
845847 } else {
@@ -880,44 +882,49 @@ mod render_tests {
880882
881883 #[ test]
882884 fn render_allow_drops_redundant_all_ports_star ( ) {
883- let r = NetRule :: parse_allow ( "udp://*:*" ) . unwrap ( ) ;
884- assert_eq ! ( format_net_rule( & r) , "udp://*" ) ;
885+ let r = & NetRule :: parse_allow ( "udp://*:*" ) . unwrap ( ) [ 0 ] ;
886+ assert_eq ! ( format_net_rule( r) , "udp://*" ) ;
885887 }
886888
887889 #[ test]
888- fn render_allow_any_ip_all_ports_tcp_is_bare_star ( ) {
889- let r = NetRule :: parse_allow ( ":*" ) . unwrap ( ) ;
890- assert_eq ! ( format_net_rule( & r) , "*" ) ;
890+ fn render_allow_any_ip_all_ports_schemeless_pair ( ) {
891+ // `:*` expands to a TCP + UDP pair; each rule renders its scheme
892+ // so the pair round-trips without widening.
893+ let rules = NetRule :: parse_allow ( ":*" ) . unwrap ( ) ;
894+ let rendered: Vec < String > = rules. iter ( ) . map ( format_net_rule) . collect ( ) ;
895+ assert_eq ! ( rendered, vec![ "tcp://*" , "udp://*" ] ) ;
891896 }
892897
893898 #[ test]
894899 fn render_allow_host_ports ( ) {
895- let r = NetRule :: parse_allow ( "example.com:443" ) . unwrap ( ) ;
896- assert_eq ! ( format_net_rule( & r) , "example.com:443" ) ;
900+ let rules = NetRule :: parse_allow ( "example.com:443" ) . unwrap ( ) ;
901+ assert_eq ! ( format_net_rule( & rules[ 0 ] ) , "tcp://example.com:443" ) ;
902+ assert_eq ! ( format_net_rule( & rules[ 1 ] ) , "udp://example.com:443" ) ;
897903 }
898904
899905 #[ test]
900906 fn render_cidr_and_ipv6_round_trip ( ) {
901907 // CIDR and IPv6-literal targets render identically for allow/deny.
902- assert_eq ! ( format_net_rule( & NetRule :: parse_allow( "10.0.0.0/8:80" ) . unwrap( ) ) , "10.0.0.0/8:80" ) ;
903- assert_eq ! ( format_net_rule( & NetRule :: parse_deny( "10.0.0.0/8" ) . unwrap( ) ) , "10.0.0.0/8" ) ;
904- assert_eq ! ( format_net_rule( & NetRule :: parse_allow( "[::1]:443" ) . unwrap( ) ) , "[::1]:443" ) ;
905- assert_eq ! ( format_net_rule( & NetRule :: parse_allow( "::1" ) . unwrap( ) ) , "::1" ) ;
908+ assert_eq ! ( format_net_rule( & NetRule :: parse_allow( "10.0.0.0/8:80" ) . unwrap( ) [ 0 ] ) , "tcp:// 10.0.0.0/8:80" ) ;
909+ assert_eq ! ( format_net_rule( & NetRule :: parse_deny( "10.0.0.0/8" ) . unwrap( ) [ 0 ] ) , "tcp:// 10.0.0.0/8" ) ;
910+ assert_eq ! ( format_net_rule( & NetRule :: parse_allow( "[::1]:443" ) . unwrap( ) [ 0 ] ) , "tcp:// [::1]:443" ) ;
911+ assert_eq ! ( format_net_rule( & NetRule :: parse_allow( "::1" ) . unwrap( ) [ 0 ] ) , "tcp:// ::1" ) ;
906912 }
907913
908914 #[ test]
909915 fn render_roundtrips_through_parse ( ) {
910916 for spec in [
911917 "example.com:443" , "udp://1.1.1.1:53" , "icmp://github.com" , "*" , "udp://*" ,
912- "10.0.0.0/8:80" , "[fc00::/7]:443" , "::1" , "1.2.3.4" ,
918+ "tcp://*" , " 10.0.0.0/8:80", "[fc00::/7]:443" , "::1" , "1.2.3.4" ,
913919 ] {
914- let r = NetRule :: parse_allow ( spec) . unwrap ( ) ;
915- let rendered = format_net_rule ( & r) ;
916- let r2 = NetRule :: parse_allow ( & rendered) . unwrap ( ) ;
917- assert_eq ! ( r. target, r2. target, "target mismatch for {spec}" ) ;
918- assert_eq ! ( r. ports, r2. ports, "ports mismatch for {spec}" ) ;
919- assert_eq ! ( r. all_ports, r2. all_ports, "all_ports mismatch for {spec}" ) ;
920- assert_eq ! ( r. protocol, r2. protocol, "protocol mismatch for {spec}" ) ;
920+ for r in & NetRule :: parse_allow ( spec) . unwrap ( ) {
921+ let rendered = format_net_rule ( r) ;
922+ // A rendered rule always carries its scheme, so it must
923+ // reparse to exactly one rule equal to the original.
924+ let reparsed = NetRule :: parse_allow ( & rendered) . unwrap ( ) ;
925+ assert_eq ! ( reparsed. len( ) , 1 , "rendered `{rendered}` from {spec} not single" ) ;
926+ assert_eq ! ( * r, reparsed[ 0 ] , "round-trip mismatch for {spec} via `{rendered}`" ) ;
927+ }
921928 }
922929 }
923930}
0 commit comments