1use rmcp::schemars::JsonSchema;
77use serde::{Deserialize, Serialize};
78use serde_json::{Value, json};
79use tailscale_cli::Invocation;
80
81use crate::cli;
82use crate::context::ToolContext;
83use crate::error::{ToolError, ToolResult};
84use crate::meta::ToolMeta;
85use crate::tools::common::{
86 Excluded, bounded_wait, document, note, object, printed, push_bool, push_text, real_path,
87 report,
88};
89
90crate::tools! {
91 tailscale_debug_derp_map => NoParams, derp_map,
98 toolset: LocalDebug, tier: Read, idempotent: true;
99
100 tailscale_debug_netmap => NoParams, netmap,
104 toolset: LocalDebug, tier: Read, idempotent: true;
105
106 tailscale_debug_hostinfo => NoParams, hostinfo,
109 toolset: LocalDebug, tier: Read, idempotent: true;
110
111 tailscale_debug_control_knobs => NoParams, control_knobs,
114 toolset: LocalDebug, tier: Read, idempotent: true;
115
116 tailscale_debug_daemon_goroutines => NoParams, daemon_goroutines,
119 toolset: LocalDebug, tier: Read, idempotent: true;
120
121 tailscale_debug_daemon_bus_graph => DaemonBusGraphParams, daemon_bus_graph,
124 toolset: LocalDebug, tier: Read, idempotent: true;
125
126 tailscale_debug_daemon_bus_queues => NoParams, daemon_bus_queues,
129 toolset: LocalDebug, tier: Read, idempotent: true;
130
131 tailscale_debug_metrics => NoParams, metrics,
134 toolset: LocalDebug, tier: Read, idempotent: true;
135
136 tailscale_debug_statedir => NoParams, statedir,
138 toolset: LocalDebug, tier: Read, idempotent: true;
139
140 tailscale_debug_go_buildinfo => NoParams, go_buildinfo,
143 toolset: LocalDebug, tier: Read, idempotent: true;
144
145 tailscale_debug_peer_relay_servers => NoParams, peer_relay_servers,
148 toolset: LocalDebug, tier: Read, idempotent: true;
149
150 tailscale_debug_peer_relay_sessions => NoParams, peer_relay_sessions,
153 toolset: LocalDebug, tier: Read, idempotent: true;
154
155 tailscale_debug_file_list => NoParams, file_list,
158 toolset: LocalDebug, tier: Read, idempotent: true;
159
160 tailscale_debug_stat => StatParams, stat,
163 toolset: LocalDebug, tier: Read, idempotent: true;
164
165 tailscale_debug_via => ViaParams, via,
169 toolset: LocalDebug, tier: Read, idempotent: true;
170
171 tailscale_debug_watch_ipn => WatchIpnParams, watch_ipn,
176 toolset: LocalDebug, tier: Read;
177
178 tailscale_debug_peer_endpoint_changes => PeerParams, peer_endpoint_changes,
181 toolset: LocalDebug, tier: Read, idempotent: true;
182
183 tailscale_debug_resolve => ResolveParams, resolve,
186 toolset: LocalDebug, tier: Read, idempotent: true;
187
188 tailscale_debug_dial_types => DialTypesParams, dial_types,
191 toolset: LocalDebug, tier: Read, idempotent: true;
192
193 tailscale_debug_derp => NoParams, derp,
196 toolset: LocalDebug, tier: Read, idempotent: true;
197
198 tailscale_debug_ts2021 => Ts2021Params, ts2021,
201 toolset: LocalDebug, tier: Read, idempotent: true;
202
203 tailscale_debug_portmap => PortmapParams, portmap,
206 toolset: LocalDebug, tier: Read, idempotent: true;
207
208 tailscale_debug_component_logs => ComponentLogsParams, component_logs,
216 toolset: LocalDebug, tier: Write;
217
218 tailscale_debug_restun => NoParams, restun,
221 toolset: LocalDebug, tier: Write, idempotent: true;
222
223 tailscale_debug_rebind => NoParams, rebind,
226 toolset: LocalDebug, tier: Write, idempotent: true;
227
228 tailscale_debug_rotate_disco_key => NoParams, rotate_disco_key,
231 toolset: LocalDebug, tier: Write;
232
233 tailscale_debug_derp_unset_on_demand => NoParams, derp_unset_on_demand,
236 toolset: LocalDebug, tier: Write, idempotent: true;
237
238 tailscale_debug_pick_new_derp => NoParams, pick_new_derp,
241 toolset: LocalDebug, tier: Write;
242
243 tailscale_debug_force_prefer_derp => ForcePreferDerpParams, force_prefer_derp,
246 toolset: LocalDebug, tier: Write, idempotent: true;
247
248 tailscale_debug_force_netmap_update => NoParams, force_netmap_update,
251 toolset: LocalDebug, tier: Write, idempotent: true;
252}
253
254pub const EXCLUDED: &[Excluded] = &[
262 Excluded {
263 path: "debug prefs",
264 reason: "it prints the node's private keys along with its preferences; \
265 `tailscale_debug_netmap` and the preference tools report the rest",
266 },
267 Excluded {
268 path: "debug local-creds",
269 reason: "it prints the credential for reaching tailscaled's private HTTP \
270 interface, which this server never uses",
271 },
272 Excluded {
273 path: "debug env",
274 reason: "it prints the whole process environment, which is where secrets \
275 for other services live",
276 },
277 Excluded {
278 path: "debug localapi",
279 reason: "it calls tailscaled's private HTTP interface directly, which this \
280 server does not do at all",
281 },
282 Excluded {
283 path: "debug daemon-logs",
284 reason: "it streams the daemon log until interrupted and has no bounded form",
285 },
286 Excluded {
287 path: "debug daemon-bus-events",
288 reason: "it streams bus events until interrupted and has no bounded form",
289 },
290 Excluded {
291 path: "debug capture",
292 reason: "it streams a packet capture of tunnel traffic until interrupted, \
293 and launches a separate application when given no path",
294 },
295 Excluded {
296 path: "debug test-risk",
297 reason: "it exists to test the client's interactive risk prompt, which a \
298 tool call cannot answer",
299 },
300 Excluded {
301 path: "debug set-expire",
302 reason: "it expires the local node's key, cutting this server off from the \
303 tailnet; re-authenticate deliberately instead",
304 },
305 Excluded {
306 path: "debug break-tcp-conns",
307 reason: "its purpose is to break the daemon's connections",
308 },
309 Excluded {
310 path: "debug break-derp-conns",
311 reason: "its purpose is to break the daemon's relay connections",
312 },
313 Excluded {
314 path: "debug derp-set-on-demand",
315 reason: "it makes the node reachable only after a delay; \
316 `tailscale_debug_derp_unset_on_demand` undoes it if something else did it",
317 },
318 Excluded {
319 path: "debug clear-netmap-cache",
320 reason: "it deletes the cached netmaps the node falls back on when the \
321 control plane is unreachable",
322 },
323 Excluded {
324 path: "debug dev-store-set",
325 reason: "it writes directly into tailscaled's state store, behind every \
326 check the rest of the client makes",
327 },
328];
329
330#[derive(Debug, Deserialize, JsonSchema)]
332pub struct NoParams {}
333
334#[derive(Debug, Serialize, JsonSchema)]
346pub struct TextReport {
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub printed: Option<String>,
349}
350
351#[derive(Debug, Serialize, JsonSchema)]
353pub struct StatEntry {
354 pub path: String,
355 pub mode: String,
356 pub size: String,
357}
358
359#[derive(Debug, Serialize, JsonSchema)]
360pub struct StatReport {
361 pub entries: Vec<StatEntry>,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 pub printed: Option<String>,
364}
365
366#[derive(Debug, Serialize, JsonSchema)]
367pub struct AddressReport {
368 pub addresses: Vec<String>,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub printed: Option<String>,
371}
372
373#[derive(Debug, Serialize, JsonSchema)]
374pub struct PathReport {
375 pub path: String,
376}
377
378#[derive(Debug, Serialize, JsonSchema)]
379pub struct ViaReport {
384 pub converted: String,
385}
386
387#[derive(Debug, Serialize, JsonSchema)]
388pub struct BusGraphReport {
389 pub format: String,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub graph: Option<Value>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub printed: Option<String>,
394}
395
396#[derive(Debug, Serialize, JsonSchema)]
397pub struct WatchReport {
398 pub events: Vec<Value>,
400 pub asked_for: u32,
403 pub seconds: u64,
405 #[serde(skip_serializing_if = "Option::is_none")]
406 pub note: Option<String>,
407}
408
409#[derive(Debug, Serialize, JsonSchema)]
415pub struct KnobReport {
416 pub outcome: String,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub printed: Option<String>,
419}
420
421#[derive(Debug, Deserialize, JsonSchema)]
426pub struct DaemonBusGraphParams {
427 #[serde(default)]
429 pub format: Option<BusGraphFormat>,
430}
431
432#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
433#[serde(rename_all = "lowercase")]
434pub enum BusGraphFormat {
435 Json,
436 Dot,
437}
438
439impl BusGraphFormat {
440 const fn as_str(self) -> &'static str {
441 match self {
442 Self::Json => "json",
443 Self::Dot => "dot",
444 }
445 }
446}
447
448#[derive(Debug, Deserialize, JsonSchema)]
449pub struct StatParams {
450 pub paths: Vec<String>,
452}
453
454#[derive(Debug, Deserialize, JsonSchema)]
455pub struct ViaParams {
456 #[serde(default)]
458 pub site_id: Option<u32>,
459 #[serde(default)]
461 pub prefix: Option<String>,
462 #[serde(default)]
465 pub route: Option<String>,
466}
467
468const DEFAULT_WATCH_SECONDS: u64 = 30;
471const MAX_WATCH_SECONDS: u64 = 300;
472const MAX_EVENTS: u32 = 100;
473
474#[derive(Debug, Deserialize, JsonSchema)]
475pub struct WatchIpnParams {
476 pub count: u32,
479 #[serde(default)]
482 pub timeout_seconds: Option<u64>,
483 #[serde(default)]
485 pub engine_updates: Option<bool>,
486 #[serde(default)]
488 pub health_actions: Option<bool>,
489 #[serde(default)]
491 pub peer_changes: Option<bool>,
492 #[serde(default)]
494 pub peer_patches: Option<bool>,
495 #[serde(default)]
497 pub peer_wireguard_state: Option<bool>,
498 #[serde(default)]
500 pub initial_client_version: Option<bool>,
501 #[serde(default)]
503 pub initial_drive_shares: Option<bool>,
504 #[serde(default)]
506 pub initial_health: Option<bool>,
507 #[serde(default)]
509 pub initial_outgoing_files: Option<bool>,
510 #[serde(default)]
512 pub initial_status: Option<bool>,
513 #[serde(default)]
515 pub initial_suggested_exit_node: Option<bool>,
516}
517
518#[derive(Debug, Deserialize, JsonSchema)]
519pub struct PeerParams {
520 pub peer: String,
522}
523
524#[derive(Debug, Deserialize, JsonSchema)]
525pub struct ResolveParams {
526 pub host: String,
528 #[serde(default)]
530 pub net: Option<String>,
531}
532
533#[derive(Debug, Deserialize, JsonSchema)]
534pub struct DialTypesParams {
535 pub host: String,
537 pub port: u16,
539 #[serde(default)]
541 pub network: Option<String>,
542}
543
544#[derive(Debug, Deserialize, JsonSchema)]
545pub struct Ts2021Params {
546 #[serde(default)]
548 pub host: Option<String>,
549 #[serde(default)]
551 pub version: Option<u32>,
552 #[serde(default)]
554 pub ace: Option<String>,
555 #[serde(default)]
557 pub dial_plan: Option<String>,
558 #[serde(default)]
560 pub verbose: Option<bool>,
561}
562
563const DEFAULT_PORTMAP_SECONDS: u64 = 5;
566const MAX_PORTMAP_SECONDS: u64 = 120;
567
568#[derive(Debug, Deserialize, JsonSchema)]
569pub struct PortmapParams {
570 #[serde(default)]
572 pub duration_seconds: Option<u64>,
573 #[serde(default)]
575 pub r#type: Option<String>,
576 #[serde(default)]
579 pub gateway_addr: Option<String>,
580 #[serde(default)]
583 pub self_addr: Option<String>,
584 #[serde(default)]
586 pub log_http: Option<bool>,
587}
588
589const DEFAULT_COMPONENT_LOG_SECONDS: i64 = 3600;
591
592const fn default_component_log_seconds() -> i64 {
593 DEFAULT_COMPONENT_LOG_SECONDS
594}
595
596#[derive(Debug, Deserialize, JsonSchema)]
597pub struct ComponentLogsParams {
598 pub component: String,
600 #[serde(default = "default_component_log_seconds")]
603 pub for_seconds: i64,
604}
605
606#[derive(Debug, Deserialize, JsonSchema)]
607pub struct ForcePreferDerpParams {
608 pub region_id: i32,
610}
611
612async fn text(ctx: &ToolContext, meta: &ToolMeta, invocation: Invocation) -> ToolResult<Value> {
618 let output = cli::run(ctx, meta, invocation).await?;
619 report(TextReport {
620 printed: printed(ctx, &output),
621 })
622}
623
624async fn knob(
626 ctx: &ToolContext,
627 meta: &ToolMeta,
628 invocation: Invocation,
629 outcome: impl Into<String>,
630) -> ToolResult<Value> {
631 let output = cli::run(ctx, meta, invocation).await?;
632 report(KnobReport {
633 outcome: outcome.into(),
634 printed: printed(ctx, &output),
635 })
636}
637
638async fn derp_map(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
643 object(
644 ctx,
645 &metas::tailscale_debug_derp_map,
646 Invocation::read(["debug", "derp-map"]),
647 )
648 .await
649}
650
651async fn netmap(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
652 object(
653 ctx,
654 &metas::tailscale_debug_netmap,
655 Invocation::read(["debug", "netmap"]),
656 )
657 .await
658}
659
660async fn hostinfo(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
661 object(
664 ctx,
665 &metas::tailscale_debug_hostinfo,
666 Invocation::read(["debug", "hostinfo"]),
667 )
668 .await
669}
670
671async fn control_knobs(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
672 object(
673 ctx,
674 &metas::tailscale_debug_control_knobs,
675 Invocation::read(["debug", "control-knobs"]),
676 )
677 .await
678}
679
680async fn daemon_goroutines(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
681 text(
682 ctx,
683 &metas::tailscale_debug_daemon_goroutines,
684 Invocation::read(["debug", "daemon-goroutines"]),
685 )
686 .await
687}
688
689async fn daemon_bus_graph(ctx: &ToolContext, params: DaemonBusGraphParams) -> ToolResult<Value> {
690 let meta = &metas::tailscale_debug_daemon_bus_graph;
691 let format = params.format.unwrap_or(BusGraphFormat::Json);
692 let invocation = Invocation::read([
693 "debug".to_owned(),
694 "daemon-bus-graph".to_owned(),
695 format!("--format={}", format.as_str()),
696 ]);
697 match format {
698 BusGraphFormat::Json => {
699 let graph = document(ctx, meta, invocation).await?;
700 report(BusGraphReport {
701 format: format.as_str().to_owned(),
702 graph: Some(graph),
703 printed: None,
704 })
705 }
706 BusGraphFormat::Dot => {
707 let output = cli::run(ctx, meta, invocation).await?;
708 report(BusGraphReport {
709 format: format.as_str().to_owned(),
710 graph: None,
711 printed: Some(output.stdout_str().trim_end().to_owned()),
712 })
713 }
714 }
715}
716
717async fn daemon_bus_queues(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
718 object(
719 ctx,
720 &metas::tailscale_debug_daemon_bus_queues,
721 Invocation::read(["debug", "daemon-bus-queues"]),
722 )
723 .await
724}
725
726async fn metrics(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
727 text(
731 ctx,
732 &metas::tailscale_debug_metrics,
733 Invocation::read(["debug", "metrics"]),
734 )
735 .await
736}
737
738async fn statedir(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
739 let path = cli::run_text(
740 ctx,
741 &metas::tailscale_debug_statedir,
742 Invocation::read(["debug", "statedir"]),
743 )
744 .await?;
745 report(PathReport {
746 path: path.trim().to_owned(),
747 })
748}
749
750async fn go_buildinfo(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
751 object(
752 ctx,
753 &metas::tailscale_debug_go_buildinfo,
754 Invocation::read(["debug", "go-buildinfo"]),
755 )
756 .await
757}
758
759async fn peer_relay_servers(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
760 object(
761 ctx,
762 &metas::tailscale_debug_peer_relay_servers,
763 Invocation::read(["debug", "peer-relay-servers"]),
764 )
765 .await
766}
767
768async fn peer_relay_sessions(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
769 text(
770 ctx,
771 &metas::tailscale_debug_peer_relay_sessions,
772 Invocation::read(["debug", "peer-relay-sessions"]),
773 )
774 .await
775}
776
777async fn file_list(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
778 let value = document(
781 ctx,
782 &metas::tailscale_debug_file_list,
783 Invocation::read(["debug", "--file=get"]),
784 )
785 .await?;
786 let files = if value.is_null() {
787 Value::Array(Vec::new())
788 } else {
789 value
790 };
791 Ok(json!({ "files": files }))
792}
793
794async fn stat(ctx: &ToolContext, params: StatParams) -> ToolResult<Value> {
795 let meta = &metas::tailscale_debug_stat;
796 if params.paths.is_empty() {
797 return Err(ToolError::invalid_args(
798 "`paths` needs at least one path to report on",
799 ));
800 }
801 let mut args = vec!["debug".to_owned(), "stat".to_owned()];
802 for path in ¶ms.paths {
803 args.push(real_path(ctx, "paths", path)?);
804 }
805 let output = cli::run(ctx, meta, Invocation::read(args)).await?;
806 report(StatReport {
807 entries: output.stdout_str().lines().filter_map(parse_stat).collect(),
808 printed: printed(ctx, &output),
809 })
810}
811
812fn parse_stat(line: &str) -> Option<StatEntry> {
817 let (path, rest) = line.trim().rsplit_once(": ")?;
818 let (mode, size) = rest.rsplit_once(", ")?;
819 Some(StatEntry {
820 path: path.to_owned(),
821 mode: mode.to_owned(),
822 size: size.to_owned(),
823 })
824}
825
826async fn via(ctx: &ToolContext, params: ViaParams) -> ToolResult<Value> {
827 let meta = &metas::tailscale_debug_via;
828 let args = match (
832 params.site_id,
833 params.prefix.as_deref(),
834 params.route.as_deref(),
835 ) {
836 (Some(site_id), Some(prefix), None) => {
837 vec![
838 "debug".to_owned(),
839 "via".to_owned(),
840 site_id.to_string(),
841 prefix.to_owned(),
842 ]
843 }
844 (None, None, Some(route)) => {
845 vec!["debug".to_owned(), "via".to_owned(), route.to_owned()]
846 }
847 (None, None, None) => {
848 return Err(ToolError::invalid_args(
849 "give `site_id` and `prefix` to build a route, or `route` to take one apart",
850 ));
851 }
852 _ => {
853 return Err(ToolError::invalid_args(
854 "`site_id` and `prefix` go together and neither goes with `route`",
855 ));
856 }
857 };
858 let text = cli::run_text(ctx, meta, Invocation::read(args)).await?;
859 report(ViaReport {
860 converted: ctx.redactor.apply(text.trim()).into_owned(),
861 })
862}
863
864async fn watch_ipn(ctx: &ToolContext, params: WatchIpnParams) -> ToolResult<Value> {
865 let meta = &metas::tailscale_debug_watch_ipn;
866 if params.count == 0 {
867 return Err(ToolError::invalid_args(
868 "`count` must be at least 1: the client reads zero as \"never stop\"",
869 ));
870 }
871 let count = params.count.min(MAX_EVENTS);
872 let (seconds, bound) = bounded_wait(
873 params.timeout_seconds,
874 DEFAULT_WATCH_SECONDS,
875 MAX_WATCH_SECONDS,
876 );
877
878 let mut args = vec![
879 "debug".to_owned(),
880 "watch-ipn".to_owned(),
881 format!("--count={count}"),
882 ];
883 push_bool(&mut args, "engine-updates", params.engine_updates);
884 push_bool(&mut args, "health-actions", params.health_actions);
885 push_bool(&mut args, "peer-changes", params.peer_changes);
886 push_bool(&mut args, "peer-patches", params.peer_patches);
887 push_bool(
888 &mut args,
889 "peer-wireguard-state",
890 params.peer_wireguard_state,
891 );
892 push_bool(
893 &mut args,
894 "initial-client-version",
895 params.initial_client_version,
896 );
897 push_bool(
898 &mut args,
899 "initial-drive-shares",
900 params.initial_drive_shares,
901 );
902 push_bool(&mut args, "initial-health", params.initial_health);
903 push_bool(
904 &mut args,
905 "initial-outgoing-files",
906 params.initial_outgoing_files,
907 );
908 push_bool(&mut args, "initial-status", params.initial_status);
909 push_bool(
910 &mut args,
911 "initial-suggested-exit-node",
912 params.initial_suggested_exit_node,
913 );
914
915 let output = cli::run(ctx, meta, Invocation::read(args).with_timeout(bound)).await?;
916 report(WatchReport {
917 events: notifications(&output.stdout_str()),
918 asked_for: count,
919 seconds,
920 note: note(ctx, &output.stderr),
921 })
922}
923
924fn notifications(stdout: &str) -> Vec<Value> {
931 serde_json::Deserializer::from_str(stdout)
932 .into_iter::<Value>()
933 .map_while(Result::ok)
934 .collect()
935}
936
937async fn peer_endpoint_changes(ctx: &ToolContext, params: PeerParams) -> ToolResult<Value> {
938 object(
939 ctx,
940 &metas::tailscale_debug_peer_endpoint_changes,
941 Invocation::read(["debug", "peer-endpoint-changes", ¶ms.peer]),
942 )
943 .await
944}
945
946async fn resolve(ctx: &ToolContext, params: ResolveParams) -> ToolResult<Value> {
947 let mut args = vec![
948 "debug".to_owned(),
949 "resolve".to_owned(),
950 params.host.clone(),
951 ];
952 push_text(&mut args, "net", params.net.as_deref());
953 let output = cli::run(ctx, &metas::tailscale_debug_resolve, Invocation::read(args)).await?;
954 report(AddressReport {
955 addresses: output
956 .stdout_str()
957 .lines()
958 .map(str::trim)
959 .filter(|l| !l.is_empty())
960 .map(str::to_owned)
961 .collect(),
962 printed: printed(ctx, &output),
963 })
964}
965
966async fn dial_types(ctx: &ToolContext, params: DialTypesParams) -> ToolResult<Value> {
967 let mut args = vec![
968 "debug".to_owned(),
969 "dial-types".to_owned(),
970 params.host.clone(),
971 params.port.to_string(),
972 ];
973 push_text(&mut args, "network", params.network.as_deref());
974 text(
975 ctx,
976 &metas::tailscale_debug_dial_types,
977 Invocation::read(args),
978 )
979 .await
980}
981
982async fn derp(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
983 text(
984 ctx,
985 &metas::tailscale_debug_derp,
986 Invocation::read(["debug", "derp"]),
987 )
988 .await
989}
990
991async fn ts2021(ctx: &ToolContext, params: Ts2021Params) -> ToolResult<Value> {
992 let mut args = vec!["debug".to_owned(), "ts2021".to_owned()];
993 push_text(&mut args, "host", params.host.as_deref());
994 push_text(&mut args, "ace", params.ace.as_deref());
995 if let Some(path) = params.dial_plan.as_deref() {
996 args.push(format!(
997 "--dial-plan={}",
998 real_path(ctx, "dial_plan", path)?
999 ));
1000 }
1001 if let Some(version) = params.version {
1002 args.push(format!("--version={version}"));
1003 }
1004 push_bool(&mut args, "verbose", params.verbose);
1005 text(ctx, &metas::tailscale_debug_ts2021, Invocation::read(args)).await
1006}
1007
1008async fn portmap(ctx: &ToolContext, params: PortmapParams) -> ToolResult<Value> {
1009 let meta = &metas::tailscale_debug_portmap;
1010 if params.gateway_addr.is_some() != params.self_addr.is_some() {
1013 return Err(ToolError::invalid_args(
1014 "`gateway_addr` and `self_addr` override the probe's view of the network \
1015 together, so give both or neither",
1016 ));
1017 }
1018 let (seconds, bound) = bounded_wait(
1019 params.duration_seconds,
1020 DEFAULT_PORTMAP_SECONDS,
1021 MAX_PORTMAP_SECONDS,
1022 );
1023 let mut args = vec![
1024 "debug".to_owned(),
1025 "portmap".to_owned(),
1026 format!("--duration={seconds}s"),
1027 ];
1028 push_text(&mut args, "type", params.r#type.as_deref());
1029 push_text(&mut args, "gateway-addr", params.gateway_addr.as_deref());
1030 push_text(&mut args, "self-addr", params.self_addr.as_deref());
1031 push_bool(&mut args, "log-http", params.log_http);
1032 text(ctx, meta, Invocation::read(args).with_timeout(bound)).await
1033}
1034
1035async fn component_logs(ctx: &ToolContext, params: ComponentLogsParams) -> ToolResult<Value> {
1040 let seconds = params.for_seconds;
1041 let outcome = if seconds > 0 {
1042 format!(
1043 "verbose logging for `{}` is on for the next {seconds}s",
1044 params.component
1045 )
1046 } else {
1047 format!("verbose logging for `{}` is off", params.component)
1048 };
1049 knob(
1050 ctx,
1051 &metas::tailscale_debug_component_logs,
1052 Invocation::mutate_shared([
1053 "debug".to_owned(),
1054 "component-logs".to_owned(),
1055 format!("--for={seconds}s"),
1056 params.component.clone(),
1057 ]),
1058 outcome,
1059 )
1060 .await
1061}
1062
1063async fn restun(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
1064 knob(
1065 ctx,
1066 &metas::tailscale_debug_restun,
1067 Invocation::mutate_shared(["debug", "restun"]),
1068 "the node was told to re-learn its own addresses",
1069 )
1070 .await
1071}
1072
1073async fn rebind(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
1074 knob(
1075 ctx,
1076 &metas::tailscale_debug_rebind,
1077 Invocation::mutate_shared(["debug", "rebind"]),
1078 "the node was told to rebind its sockets",
1079 )
1080 .await
1081}
1082
1083async fn rotate_disco_key(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
1084 knob(
1085 ctx,
1086 &metas::tailscale_debug_rotate_disco_key,
1087 Invocation::mutate_shared(["debug", "rotate-disco-key"]),
1088 "the node's disco key was rotated; peers will rebuild their direct paths",
1089 )
1090 .await
1091}
1092
1093async fn derp_unset_on_demand(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
1094 knob(
1095 ctx,
1096 &metas::tailscale_debug_derp_unset_on_demand,
1097 Invocation::mutate_shared(["debug", "derp-unset-on-demand"]),
1098 "relay connections are back to always-on",
1099 )
1100 .await
1101}
1102
1103async fn pick_new_derp(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
1104 knob(
1105 ctx,
1106 &metas::tailscale_debug_pick_new_derp,
1107 Invocation::mutate_shared(["debug", "pick-new-derp"]),
1108 "the node was moved to another home relay region until it restarts",
1109 )
1110 .await
1111}
1112
1113async fn force_prefer_derp(ctx: &ToolContext, params: ForcePreferDerpParams) -> ToolResult<Value> {
1114 let outcome = if params.region_id == 0 {
1115 "the node is free to choose its own home relay region again".to_owned()
1116 } else {
1117 format!(
1118 "the node prefers relay region {} until it restarts",
1119 params.region_id
1120 )
1121 };
1122 knob(
1123 ctx,
1124 &metas::tailscale_debug_force_prefer_derp,
1125 Invocation::mutate_shared([
1126 "debug".to_owned(),
1127 "force-prefer-derp".to_owned(),
1128 params.region_id.to_string(),
1129 ]),
1130 outcome,
1131 )
1132 .await
1133}
1134
1135async fn force_netmap_update(ctx: &ToolContext, _params: NoParams) -> ToolResult<Value> {
1136 knob(
1137 ctx,
1138 &metas::tailscale_debug_force_netmap_update,
1139 Invocation::mutate_shared(["debug", "force-netmap-update"]),
1140 "a full netmap update was pushed through the daemon",
1141 )
1142 .await
1143}
1144
1145#[allow(clippy::unwrap_used, clippy::expect_used)]
1146#[cfg(test)]
1147mod tests {
1148 use std::collections::BTreeSet;
1149 use std::sync::Arc;
1150 use std::time::Duration;
1151
1152 use super::*;
1153
1154 use crate::error::ErrorCode;
1155 use crate::testing::{Reply, StubBackend, context};
1156
1157 async fn against<F, P, Fut>(reply: Reply, handler: F, params: P) -> (Value, Vec<Vec<String>>)
1160 where
1161 F: FnOnce(ToolContext, P) -> Fut,
1162 Fut: std::future::Future<Output = ToolResult<Value>>,
1163 {
1164 let backend = Arc::new(StubBackend::always(reply));
1165 let ctx = context(Arc::clone(&backend));
1166 let value = handler(ctx, params).await.expect("the handler succeeds");
1167 (value, backend.argv())
1168 }
1169
1170 async fn refusal<F, P, Fut>(reply: Reply, handler: F, params: P) -> ToolError
1172 where
1173 F: FnOnce(ToolContext, P) -> Fut,
1174 Fut: std::future::Future<Output = ToolResult<Value>>,
1175 {
1176 let backend = Arc::new(StubBackend::always(reply));
1177 let ctx = context(backend);
1178 handler(ctx, params).await.expect_err("the handler refuses")
1179 }
1180
1181 fn watching(count: u32, timeout_seconds: Option<u64>) -> WatchIpnParams {
1182 WatchIpnParams {
1183 count,
1184 timeout_seconds,
1185 engine_updates: None,
1186 health_actions: None,
1187 peer_changes: None,
1188 peer_patches: None,
1189 peer_wireguard_state: None,
1190 initial_client_version: None,
1191 initial_drive_shares: None,
1192 initial_health: None,
1193 initial_outgoing_files: None,
1194 initial_status: None,
1195 initial_suggested_exit_node: None,
1196 }
1197 }
1198
1199 #[test]
1200 fn the_excluded_list_is_complete_and_says_why() {
1201 assert_eq!(
1202 EXCLUDED.len(),
1203 14,
1204 "fourteen of the forty-four are excluded"
1205 );
1206 let mut paths = BTreeSet::new();
1207 for excluded in EXCLUDED {
1208 assert!(
1209 excluded.path.starts_with("debug "),
1210 "`{}` is not a debug subcommand",
1211 excluded.path
1212 );
1213 assert!(
1214 excluded.reason.len() > 20,
1215 "`{}` is excluded without saying why",
1216 excluded.path
1217 );
1218 assert!(
1219 paths.insert(excluded.path),
1220 "`{}` is excluded twice",
1221 excluded.path
1222 );
1223 }
1224 }
1225
1226 #[test]
1227 fn a_stat_line_splits_from_the_right() {
1228 let entry = parse_stat("/etc/hosts: -rw-r--r--, 213").expect("a well-formed line");
1229 assert_eq!(entry.path, "/etc/hosts");
1230 assert_eq!(entry.mode, "-rw-r--r--");
1231 assert_eq!(entry.size, "213");
1232
1233 let odd = parse_stat("/tmp/a: b, c/file: -rw-------, 0").expect("a path with separators");
1235 assert_eq!(odd.path, "/tmp/a: b, c/file");
1236 assert_eq!(odd.size, "0");
1237
1238 assert!(parse_stat("something else entirely").is_none());
1239 }
1240
1241 #[test]
1242 fn concatenated_notifications_are_read_one_at_a_time() {
1243 let stream = "{\n\t\"Version\": \"1.102.2\"\n}\n{\n\t\"Version\": \"1.102.2\"\n}\n";
1246 assert!(serde_json::from_str::<Value>(stream).is_err());
1247
1248 let events = notifications(stream);
1249 assert_eq!(events.len(), 2);
1250 assert_eq!(events[0]["Version"], "1.102.2");
1251 }
1252
1253 #[test]
1254 fn a_half_written_notification_is_dropped_rather_than_losing_the_rest() {
1255 let stream = "{\"a\": 1}\n{\"b\": 2}\n{\"c\":";
1256 let events = notifications(stream);
1257 assert_eq!(
1258 events.len(),
1259 2,
1260 "the whole objects survive the truncated one"
1261 );
1262 }
1263
1264 #[tokio::test]
1267 async fn the_watcher_honours_its_count_and_its_cap() {
1268 let (value, argv) = against(
1269 Reply::ok("{\"Version\":\"1.102.2\"}\n"),
1270 |ctx, p| async move { watch_ipn(&ctx, p).await },
1271 watching(3, None),
1272 )
1273 .await;
1274 assert_eq!(argv, [["debug", "watch-ipn", "--count=3"]]);
1275 assert_eq!(value["asked_for"], 3);
1276 assert_eq!(value["seconds"], DEFAULT_WATCH_SECONDS);
1277 assert_eq!(value["events"].as_array().map(Vec::len), Some(1));
1278
1279 let (value, argv) = against(
1280 Reply::ok(""),
1281 |ctx, p| async move { watch_ipn(&ctx, p).await },
1282 watching(10_000, Some(10_000)),
1283 )
1284 .await;
1285 assert_eq!(
1286 argv,
1287 [["debug", "watch-ipn", &format!("--count={MAX_EVENTS}")]]
1288 );
1289 assert_eq!(value["asked_for"], MAX_EVENTS);
1290 assert_eq!(value["seconds"], MAX_WATCH_SECONDS);
1291 }
1292
1293 #[tokio::test]
1294 async fn the_watcher_and_the_portmap_probe_bound_the_process_too() {
1295 let backend = Arc::new(StubBackend::always(Reply::ok("")));
1296 let ctx = context(Arc::clone(&backend));
1297 watch_ipn(&ctx, watching(3, None))
1298 .await
1299 .expect("the watcher succeeds");
1300 portmap(
1301 &ctx,
1302 PortmapParams {
1303 duration_seconds: None,
1304 r#type: None,
1305 gateway_addr: None,
1306 self_addr: None,
1307 log_http: None,
1308 },
1309 )
1310 .await
1311 .expect("the probe succeeds");
1312 let bounds: Vec<Duration> = backend.calls().iter().map(|call| call.timeout).collect();
1313 assert_eq!(
1314 bounds,
1315 [
1316 Duration::from_secs(DEFAULT_WATCH_SECONDS + 5),
1317 Duration::from_secs(DEFAULT_PORTMAP_SECONDS + 5)
1318 ]
1319 );
1320 }
1321
1322 #[tokio::test]
1325 async fn the_bus_graph_reports_a_document_or_a_drawing_but_never_both() {
1326 let (value, argv) = against(
1327 Reply::ok(r#"{"nodes":[]}"#),
1328 |ctx, p| async move { daemon_bus_graph(&ctx, p).await },
1329 DaemonBusGraphParams { format: None },
1330 )
1331 .await;
1332 assert_eq!(argv, [["debug", "daemon-bus-graph", "--format=json"]]);
1333 assert_eq!(value["format"], "json");
1334 assert!(value["graph"].is_object());
1335 assert!(value.get("printed").is_none());
1336
1337 let (value, argv) = against(
1338 Reply::ok("digraph {}\n"),
1339 |ctx, p| async move { daemon_bus_graph(&ctx, p).await },
1340 DaemonBusGraphParams {
1341 format: Some(BusGraphFormat::Dot),
1342 },
1343 )
1344 .await;
1345 assert_eq!(argv, [["debug", "daemon-bus-graph", "--format=dot"]]);
1346 assert_eq!(value["format"], "dot");
1347 assert_eq!(value["printed"], "digraph {}");
1348 assert!(value.get("graph").is_none());
1349 }
1350
1351 #[tokio::test]
1352 async fn an_empty_inbox_is_a_list_of_nothing_rather_than_a_null() {
1353 let (value, argv) = against(
1354 Reply::ok("null\n"),
1355 |ctx, p| async move { file_list(&ctx, p).await },
1356 NoParams {},
1357 )
1358 .await;
1359 assert_eq!(argv, [["debug", "--file=get"]]);
1360 assert_eq!(value["files"], json!([]));
1361 }
1362
1363 #[tokio::test]
1364 async fn the_two_conversions_via_offers_are_told_apart_before_anything_runs() {
1365 let (value, argv) = against(
1366 Reply::ok("fd7a:115c:a1e0:b1a:0:7:a01:0/112\n"),
1367 |ctx, p| async move { via(&ctx, p).await },
1368 ViaParams {
1369 site_id: Some(7),
1370 prefix: Some("10.1.0.0/16".to_owned()),
1371 route: None,
1372 },
1373 )
1374 .await;
1375 assert_eq!(argv, [["debug", "via", "7", "10.1.0.0/16"]]);
1376 assert_eq!(value["converted"], "fd7a:115c:a1e0:b1a:0:7:a01:0/112");
1377
1378 let (_, argv) = against(
1379 Reply::ok("site 7 (0x7), 10.1.0.0/16\n"),
1380 |ctx, p| async move { via(&ctx, p).await },
1381 ViaParams {
1382 site_id: None,
1383 prefix: None,
1384 route: Some("fd7a:115c:a1e0:b1a:0:7:a01:0/112".to_owned()),
1385 },
1386 )
1387 .await;
1388 assert_eq!(argv, [["debug", "via", "fd7a:115c:a1e0:b1a:0:7:a01:0/112"]]);
1389
1390 for mixture in [
1391 ViaParams {
1392 site_id: Some(7),
1393 prefix: None,
1394 route: None,
1395 },
1396 ViaParams {
1397 site_id: Some(7),
1398 prefix: Some("10.1.0.0/16".to_owned()),
1399 route: Some("fd7a::/112".to_owned()),
1400 },
1401 ViaParams {
1402 site_id: None,
1403 prefix: None,
1404 route: None,
1405 },
1406 ] {
1407 let error = refusal(
1408 Reply::ok(""),
1409 |ctx, p| async move { via(&ctx, p).await },
1410 mixture,
1411 )
1412 .await;
1413 assert_eq!(error.code, ErrorCode::InvalidArgs);
1414 }
1415 }
1416
1417 #[tokio::test]
1418 async fn the_portmap_overrides_are_refused_one_at_a_time() {
1419 for lonely in [
1422 PortmapParams {
1423 duration_seconds: None,
1424 r#type: None,
1425 gateway_addr: Some("192.0.2.1".to_owned()),
1426 self_addr: None,
1427 log_http: None,
1428 },
1429 PortmapParams {
1430 duration_seconds: None,
1431 r#type: None,
1432 gateway_addr: None,
1433 self_addr: Some("192.0.2.2".to_owned()),
1434 log_http: None,
1435 },
1436 ] {
1437 let error = refusal(
1438 Reply::ok(""),
1439 |ctx, p| async move { portmap(&ctx, p).await },
1440 lonely,
1441 )
1442 .await;
1443 assert_eq!(error.code, ErrorCode::InvalidArgs);
1444 }
1445
1446 let (value, argv) = against(
1447 Reply::ok("portmapper: no port mapping services were found\n"),
1448 |ctx, p| async move { portmap(&ctx, p).await },
1449 PortmapParams {
1450 duration_seconds: Some(10_000),
1451 r#type: Some("upnp".to_owned()),
1452 gateway_addr: Some("192.0.2.1".to_owned()),
1453 self_addr: Some("192.0.2.2".to_owned()),
1454 log_http: Some(false),
1455 },
1456 )
1457 .await;
1458 assert_eq!(
1459 argv,
1460 [[
1461 "debug",
1462 "portmap",
1463 &format!("--duration={MAX_PORTMAP_SECONDS}s"),
1464 "--type=upnp",
1465 "--gateway-addr=192.0.2.1",
1466 "--self-addr=192.0.2.2",
1467 "--log-http=false"
1468 ]]
1469 );
1470 assert!(
1471 value["printed"]
1472 .as_str()
1473 .is_some_and(|p| p.contains("portmapper"))
1474 );
1475 }
1476
1477 #[tokio::test]
1478 async fn turning_component_logging_off_says_so_rather_than_saying_how_long_for() {
1479 let (value, argv) = against(
1480 Reply::ok(""),
1481 |ctx, p| async move { component_logs(&ctx, p).await },
1482 ComponentLogsParams {
1483 component: "magicsock".to_owned(),
1484 for_seconds: default_component_log_seconds(),
1485 },
1486 )
1487 .await;
1488 assert_eq!(
1489 argv,
1490 [[
1491 "debug",
1492 "component-logs",
1493 &format!("--for={DEFAULT_COMPONENT_LOG_SECONDS}s"),
1494 "magicsock"
1495 ]]
1496 );
1497 assert!(
1498 value["outcome"]
1499 .as_str()
1500 .is_some_and(|o| o.contains("is on"))
1501 );
1502
1503 let (value, argv) = against(
1504 Reply::ok(""),
1505 |ctx, p| async move { component_logs(&ctx, p).await },
1506 ComponentLogsParams {
1507 component: "magicsock".to_owned(),
1508 for_seconds: 0,
1509 },
1510 )
1511 .await;
1512 assert_eq!(argv, [["debug", "component-logs", "--for=0s", "magicsock"]]);
1513 assert_eq!(value["outcome"], "verbose logging for `magicsock` is off");
1514 }
1515
1516 #[tokio::test]
1517 async fn pinning_a_relay_region_and_letting_it_go_read_differently() {
1518 let (value, argv) = against(
1519 Reply::ok(""),
1520 |ctx, p| async move { force_prefer_derp(&ctx, p).await },
1521 ForcePreferDerpParams { region_id: 0 },
1522 )
1523 .await;
1524 assert_eq!(argv, [["debug", "force-prefer-derp", "0"]]);
1525 assert!(
1526 value["outcome"]
1527 .as_str()
1528 .is_some_and(|o| o.contains("free to choose"))
1529 );
1530
1531 let (value, _) = against(
1532 Reply::ok(""),
1533 |ctx, p| async move { force_prefer_derp(&ctx, p).await },
1534 ForcePreferDerpParams { region_id: 2 },
1535 )
1536 .await;
1537 assert!(
1538 value["outcome"]
1539 .as_str()
1540 .is_some_and(|o| o.contains("region 2"))
1541 );
1542 }
1543
1544 #[tokio::test]
1545 async fn stat_refuses_an_empty_list_and_a_stream() {
1546 let error = refusal(
1547 Reply::ok(""),
1548 |ctx, p| async move { stat(&ctx, p).await },
1549 StatParams { paths: Vec::new() },
1550 )
1551 .await;
1552 assert_eq!(error.code, ErrorCode::InvalidArgs);
1553
1554 let error = refusal(
1557 Reply::ok(""),
1558 |ctx, p| async move { stat(&ctx, p).await },
1559 StatParams {
1560 paths: vec!["-".to_owned()],
1561 },
1562 )
1563 .await;
1564 assert_eq!(error.code, ErrorCode::InvalidArgs);
1565 }
1566}