@@ -93,15 +93,7 @@ pub struct WallhackCli {
9393 #[ argh( option) ]
9494 pub max_peers : Option < usize > ,
9595
96- /// prefer a role during auto-negotiation (entry, exit, relay)
97- #[ argh( option) ]
98- pub prefer_role : Option < String > ,
99-
100- /// exclude a role during auto-negotiation (entry, exit, relay)
101- #[ argh( option) ]
102- pub exclude_role : Option < String > ,
103-
104- /// override the negotiated role (entry, exit, relay)
96+ /// set role: entry, exit, relay, prefer:entry, exclude:relay, or auto
10597 #[ argh( option) ]
10698 pub role : Option < String > ,
10799
@@ -162,13 +154,13 @@ pub struct CliError {
162154/// Configuration build error.
163155#[ derive( Debug , thiserror:: Error , PartialEq ) ]
164156pub enum ConfigError {
165- #[ error( "--prefer, --exclude-role, and --role are mutually exclusive" ) ]
166- HintFlagsConflict ,
167157 #[ error( "--role entry requires TUN capability (CAP_NET_ADMIN)" ) ]
168158 RoleEntryRequiresTun ,
169159 #[ error( "--role relay requires both --connect and --listen" ) ]
170160 RoleRelayRequiresConnectAndListen ,
171- #[ error( "invalid role '{0}': expected 'entry', 'exit', or 'relay'" ) ]
161+ #[ error(
162+ "invalid role '{0}': expected entry, exit, relay, prefer:entry, exclude:relay, or auto"
163+ ) ]
172164 InvalidRole ( String ) ,
173165 #[ error( "invalid address '{0}'" ) ]
174166 InvalidAddress ( String ) ,
@@ -216,29 +208,39 @@ fn parse_role(s: &str) -> Result<ProtoNodeRole, ConfigError> {
216208 }
217209}
218210
219- /// Build a `RoleHint` from the mutually-exclusive hint CLI flags.
211+ /// Parse `--role` value into a `RoleHint`.
212+ ///
213+ /// Accepted formats:
214+ /// - `entry` / `exit` / `relay` — fixed role (shorthand for `fixed:entry`)
215+ /// - `prefer:entry` / `exclude:relay` / `fixed:exit` — explicit level
216+ /// - `auto` — returns `None` (clear all hints)
220217fn resolve_hint ( cli : & WallhackCli ) -> Result < Option < RoleHint > , ConfigError > {
221- let hints: Vec < _ > = [
222- cli. prefer_role . as_deref ( ) . map ( |s| ( HintLevel :: Prefer , s) ) ,
223- cli. exclude_role . as_deref ( ) . map ( |s| ( HintLevel :: Exclude , s) ) ,
224- cli. role . as_deref ( ) . map ( |s| ( HintLevel :: Fixed , s) ) ,
225- ]
226- . into_iter ( )
227- . flatten ( )
228- . collect ( ) ;
229-
230- match hints. len ( ) {
231- 0 => Ok ( None ) ,
232- 1 => {
233- let ( level, role_str) = hints[ 0 ] ;
234- let target = parse_role ( role_str) ?;
235- Ok ( Some ( RoleHint {
236- level : level. into ( ) ,
237- target : target. into ( ) ,
238- } ) )
239- }
240- _ => Err ( ConfigError :: HintFlagsConflict ) ,
218+ let Some ( ref role_str) = cli. role else {
219+ return Ok ( None ) ;
220+ } ;
221+
222+ if role_str == "auto" {
223+ return Ok ( None ) ;
241224 }
225+
226+ let ( level, role_part) = if let Some ( ( level_str, role) ) = role_str. split_once ( ':' ) {
227+ let level = match level_str {
228+ "prefer" => HintLevel :: Prefer ,
229+ "exclude" => HintLevel :: Exclude ,
230+ "fixed" => HintLevel :: Fixed ,
231+ _ => return Err ( ConfigError :: InvalidRole ( role_str. clone ( ) ) ) ,
232+ } ;
233+ ( level, role)
234+ } else {
235+ // Bare role name = fixed
236+ ( HintLevel :: Fixed , role_str. as_str ( ) )
237+ } ;
238+
239+ let target = parse_role ( role_part) ?;
240+ Ok ( Some ( RoleHint {
241+ level : level. into ( ) ,
242+ target : target. into ( ) ,
243+ } ) )
242244}
243245
244246/// Resolve PSK from flag or `WALLHACK_PSK` environment variable.
@@ -370,23 +372,6 @@ mod tests {
370372 parse_cli_from_args ( & v)
371373 }
372374
373- #[ test]
374- fn mutually_exclusive_hint_flags ( ) {
375- let c = cli ( & [
376- "--prefer-role" ,
377- "entry" ,
378- "--role" ,
379- "exit" ,
380- "--listen" ,
381- ":6565" ,
382- ] )
383- . unwrap ( ) ;
384- assert_eq ! (
385- build_daemon_config( & c) . unwrap_err( ) ,
386- ConfigError :: HintFlagsConflict
387- ) ;
388- }
389-
390375 #[ test]
391376 fn role_entry_requires_tun ( ) {
392377 // Only testable on machines without CAP_NET_ADMIN.
@@ -409,8 +394,8 @@ mod tests {
409394 }
410395
411396 #[ test]
412- fn valid_prefer_hint_produces_auto_config ( ) {
413- let c = cli ( & [ "--prefer- role" , "entry" , "--listen" , ":6565" ] ) . unwrap ( ) ;
397+ fn prefer_colon_syntax ( ) {
398+ let c = cli ( & [ "--role" , "prefer: entry" , "--listen" , ":6565" ] ) . unwrap ( ) ;
414399 let config = build_daemon_config ( & c) . unwrap ( ) ;
415400 match & config. mode {
416401 ModeConfig :: Auto ( auto) => {
@@ -422,12 +407,59 @@ mod tests {
422407 }
423408 }
424409
410+ #[ test]
411+ fn exclude_colon_syntax ( ) {
412+ let c = cli ( & [ "--role" , "exclude:relay" , "--listen" , ":6565" ] ) . unwrap ( ) ;
413+ let config = build_daemon_config ( & c) . unwrap ( ) ;
414+ match & config. mode {
415+ ModeConfig :: Auto ( auto) => {
416+ let hint = auto. hint . as_ref ( ) . expect ( "hint should be set" ) ;
417+ assert_eq ! ( hint. level, i32 :: from( HintLevel :: Exclude ) ) ;
418+ assert_eq ! ( hint. target, i32 :: from( ProtoNodeRole :: RoleRelay ) ) ;
419+ }
420+ other => panic ! ( "expected Auto, got {other:?}" ) ,
421+ }
422+ }
423+
424+ #[ test]
425+ fn bare_role_is_fixed ( ) {
426+ let c = cli ( & [ "--role" , "exit" , "--connect" , "host:443" ] ) . unwrap ( ) ;
427+ let config = build_daemon_config ( & c) . unwrap ( ) ;
428+ match & config. mode {
429+ ModeConfig :: Auto ( auto) => {
430+ let hint = auto. hint . as_ref ( ) . expect ( "hint should be set" ) ;
431+ assert_eq ! ( hint. level, i32 :: from( HintLevel :: Fixed ) ) ;
432+ assert_eq ! ( hint. target, i32 :: from( ProtoNodeRole :: RoleExit ) ) ;
433+ }
434+ other => panic ! ( "expected Auto, got {other:?}" ) ,
435+ }
436+ }
437+
438+ #[ test]
439+ fn role_auto_clears_hint ( ) {
440+ let c = cli ( & [ "--role" , "auto" , "--listen" , ":6565" ] ) . unwrap ( ) ;
441+ let config = build_daemon_config ( & c) . unwrap ( ) ;
442+ match & config. mode {
443+ ModeConfig :: Auto ( auto) => assert ! ( auto. hint. is_none( ) ) ,
444+ other => panic ! ( "expected Auto, got {other:?}" ) ,
445+ }
446+ }
447+
425448 #[ test]
426449 fn invalid_role_string_rejected ( ) {
427- let c = cli ( & [ "--prefer- role" , "bogus" , "--listen" , ":6565" ] ) . unwrap ( ) ;
428- assert_eq ! (
450+ let c = cli ( & [ "--role" , "bogus" , "--listen" , ":6565" ] ) . unwrap ( ) ;
451+ assert ! ( matches !(
429452 build_daemon_config( & c) . unwrap_err( ) ,
430- ConfigError :: InvalidRole ( "bogus" . to_string( ) )
431- ) ;
453+ ConfigError :: InvalidRole ( _)
454+ ) ) ;
455+ }
456+
457+ #[ test]
458+ fn invalid_colon_level_rejected ( ) {
459+ let c = cli ( & [ "--role" , "bogus:entry" , "--listen" , ":6565" ] ) . unwrap ( ) ;
460+ assert ! ( matches!(
461+ build_daemon_config( & c) . unwrap_err( ) ,
462+ ConfigError :: InvalidRole ( _)
463+ ) ) ;
432464 }
433465}
0 commit comments