Skip to main content

mythic/protocol/
peer.rs

1//! P2P and auxiliary message types — delegates, SOCKS, reverse port forward,
2//! interactive tasking, alerts, and edges.
3
4use alloc::string::{String, ToString};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
10pub struct DelegateMessage {
11    pub message: String,
12    /// Required in agent-to-Mythic delegate messages; absent in Mythic-to-agent responses.
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub c2_profile: Option<String>,
15    pub uuid: Uuid,
16    #[serde(default, skip_serializing_if = "Option::is_none", alias = "new_uuid")]
17    pub mythic_uuid: Option<Uuid>,
18}
19
20pub type P2PMessage = DelegateMessage;
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23pub struct AlertMessage {
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub source: Option<String>,
26    #[serde(default = "default_alert_level", skip_serializing_if = "is_warning")]
27    pub level: Option<String>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub alert: Option<String>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub send_webhook: Option<bool>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub webhook_alert: Option<Value>,
34}
35
36impl Default for AlertMessage {
37    fn default() -> Self {
38        Self {
39            source: None,
40            level: default_alert_level(),
41            alert: None,
42            send_webhook: None,
43            webhook_alert: None,
44        }
45    }
46}
47
48fn default_alert_level() -> Option<String> {
49    Some("warning".to_string())
50}
51
52fn is_warning(level: &Option<String>) -> bool {
53    matches!(level.as_deref(), Some("warning"))
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
57pub struct EdgeMessage {
58    pub source: String,
59    pub destination: String,
60    pub action: String,
61    pub c2_profile: String,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub metadata: Option<String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
67pub struct SocksMessage {
68    pub server_id: u32,
69    pub exit: bool,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub data: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
75pub struct ReversePortForwardMessage {
76    pub server_id: u32,
77    pub exit: bool,
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub data: Option<String>,
80    /// Optional — required when the agent listens on multiple rpfwd ports so
81    /// Mythic can route data to the correct remote IP:Port.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub port: Option<u32>,
84}
85
86pub type RpfwdMessage = ReversePortForwardMessage;
87
88#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
89pub struct InteractiveMessage {
90    pub task_id: Uuid,
91    pub data: String,
92    pub message_type: u8,
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use alloc::string::ToString;
99
100    #[test]
101    fn peer_messages_roundtrip() {
102        let uuid = Uuid::nil();
103        let next_uuid = Uuid::from_u128(1);
104
105        let delegate = DelegateMessage {
106            message: "msg".to_string(),
107            c2_profile: Some("p2p".to_string()),
108            uuid,
109            mythic_uuid: Some(next_uuid),
110        };
111        assert_eq!(
112            serde_json::from_str::<DelegateMessage>(&serde_json::to_string(&delegate).unwrap())
113                .unwrap(),
114            delegate
115        );
116
117        let alert = AlertMessage {
118            source: Some("src".to_string()),
119            level: Some("low".to_string()),
120            alert: Some("warn".to_string()),
121            send_webhook: Some(true),
122            webhook_alert: Some(serde_json::json!({"a": 1})),
123        };
124        assert_eq!(
125            serde_json::from_str::<AlertMessage>(&serde_json::to_string(&alert).unwrap()).unwrap(),
126            alert
127        );
128
129        let edge = EdgeMessage {
130            source: "src".to_string(),
131            destination: "dst".to_string(),
132            action: "link".to_string(),
133            c2_profile: "http".to_string(),
134            metadata: Some("{}".to_string()),
135        };
136        assert_eq!(
137            serde_json::from_str::<EdgeMessage>(&serde_json::to_string(&edge).unwrap()).unwrap(),
138            edge
139        );
140
141        let socks = SocksMessage {
142            server_id: 9,
143            exit: false,
144            data: Some("d".to_string()),
145        };
146        assert_eq!(
147            serde_json::from_str::<SocksMessage>(&serde_json::to_string(&socks).unwrap()).unwrap(),
148            socks
149        );
150
151        let rpfwd = ReversePortForwardMessage {
152            server_id: 3,
153            exit: true,
154            data: None,
155            port: Some(80),
156        };
157        assert_eq!(
158            serde_json::from_str::<ReversePortForwardMessage>(
159                &serde_json::to_string(&rpfwd).unwrap()
160            )
161            .unwrap(),
162            rpfwd
163        );
164
165        let interactive = InteractiveMessage {
166            task_id: next_uuid,
167            data: "abc".to_string(),
168            message_type: 1,
169        };
170        assert_eq!(
171            serde_json::from_str::<InteractiveMessage>(
172                &serde_json::to_string(&interactive).unwrap()
173            )
174            .unwrap(),
175            interactive
176        );
177
178        let minimal_alert: AlertMessage = serde_json::from_str(r#"{"alert":"hello"}"#).unwrap();
179        assert_eq!(minimal_alert.alert.as_deref(), Some("hello"));
180        assert!(minimal_alert.source.is_none());
181        assert_eq!(minimal_alert.level.as_deref(), Some("warning"));
182
183        let socks_json: SocksMessage =
184            serde_json::from_str(r#"{"server_id":1,"exit":true}"#).unwrap();
185        assert!(socks_json.exit);
186        assert!(socks_json.data.is_none());
187
188        let rpfwd_json: ReversePortForwardMessage =
189            serde_json::from_str(r#"{"server_id":2,"exit":false,"data":"YQ"}"#).unwrap();
190        assert!(!rpfwd_json.exit);
191        assert_eq!(rpfwd_json.data.as_deref(), Some("YQ"));
192        assert!(rpfwd_json.port.is_none());
193
194        let rpfwd_with_port: ReversePortForwardMessage =
195            serde_json::from_str(r#"{"server_id":3,"exit":true,"data":"","port":445}"#).unwrap();
196        assert_eq!(rpfwd_with_port.port, Some(445));
197    }
198}