1use super::frame::{self, Encap, Frame, Mac, Payload};
5use crate::llm::actions::{
6 protocol_trait::{ActionResult, Protocol, Server},
7 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
8};
9use crate::protocol::{log_template::LogTemplate, EventType, SpawnContext};
10use crate::state::app_state::AppState;
11use anyhow::{bail, Context, Result};
12use serde_json::{json, Value};
13use std::collections::HashMap;
14use std::net::Ipv4Addr;
15use std::sync::{Arc, LazyLock, Mutex};
16
17pub const DEFAULT_ENCAPSULATION: &str = "vxlan";
18
19#[derive(Clone)]
21pub struct FrameContext {
22 pub encap: Encap,
23 pub header: frame::Header,
26 pub frame: Frame,
27 pub default_mac: Mac,
28 pub macs: Arc<Mutex<HashMap<Ipv4Addr, Mac>>>,
30}
31
32#[derive(Default, Clone)]
33pub struct VxlanProtocol {
34 request: Option<FrameContext>,
35}
36
37impl VxlanProtocol {
38 pub fn new() -> Self {
39 Self::default()
40 }
41 pub fn for_frame(ctx: FrameContext) -> Self {
42 Self { request: Some(ctx) }
43 }
44}
45
46pub fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
47 Parameter {
48 name: name.into(),
49 type_hint: type_hint.into(),
50 description: description.into(),
51 required,
52 }
53}
54
55pub fn action(
56 name: &str,
57 description: &str,
58 parameters: Vec<Parameter>,
59 example: Value,
60) -> ActionDefinition {
61 let info = match name {
62 "vxlan_arp_reply" => "-> overlay ARP reply {mac}".to_string(),
63 "vxlan_icmp_echo_reply" => "-> overlay echo reply".to_string(),
64 "vxlan_udp_reply" => "-> overlay UDP reply".to_string(),
65 other => format!("-> overlay {}", other.trim_start_matches("vxlan_")),
66 };
67 ActionDefinition {
68 name: name.into(),
69 description: description.into(),
70 parameters,
71 example,
72 log_template: Some(LogTemplate::new().with_info(info)),
73 }
74}
75
76pub fn arp_reply_action() -> ActionDefinition {
77 action(
78 "vxlan_arp_reply",
79 "Answer the ARP request: the target IP is one of your overlay hosts, at this MAC. To say no host has it, answer nothing.",
80 vec![p("mac", "string", "The host's MAC, e.g. 02:4e:47:00:00:02 (default: the server's overlay_mac)", false)],
81 json!({"type":"vxlan_arp_reply","mac":"02:4e:47:00:00:02"}),
82 )
83}
84
85pub fn echo_reply_action() -> ActionDefinition {
86 action(
87 "vxlan_icmp_echo_reply",
88 "Answer the ping: an echo reply with the request's identifier, sequence and data.",
89 vec![],
90 json!({"type":"vxlan_icmp_echo_reply"}),
91 )
92}
93
94pub fn data_params() -> Vec<Parameter> {
95 vec![
96 p(
97 "data",
98 "string",
99 "The payload, as text (or hex with encoding hex)",
100 true,
101 ),
102 p("encoding", "string", "utf8 (default) or hex", false),
103 ]
104}
105
106pub fn udp_reply_action() -> ActionDefinition {
107 action(
108 "vxlan_udp_reply",
109 "Answer the UDP datagram from the port it was sent to, back to its sender.",
110 data_params(),
111 json!({"type":"vxlan_udp_reply","data":"pong","encoding":"utf8"}),
112 )
113}
114
115fn common_params() -> Vec<Parameter> {
116 vec![
117 p(
118 "vni",
119 "number",
120 "The overlay network: the VXLAN/Geneve Network Identifier, or the GRE key (null for GRE without a key)",
121 false,
122 ),
123 p(
124 "vtep",
125 "string",
126 "The tunnel endpoint the frame came from, ip:port",
127 true,
128 ),
129 p("src_mac", "string", "The sending host's MAC", true),
130 ]
131}
132
133pub static ARP_EVENT: LazyLock<EventType> = LazyLock::new(|| {
134 let mut params = common_params();
135 params.push(p("sender_ip", "string", "The asking host's IP", true));
136 params.push(p("target_ip", "string", "The IP it is looking for", true));
137 EventType::new(
138 "vxlan_arp_request",
139 "A host on the overlay asks who has target_ip. Answer vxlan_arp_reply if one of your hosts does, or nothing.",
140 arp_reply_action().example.clone(),
141 )
142 .with_parameters(params)
143 .with_actions(vec![arp_reply_action()])
144});
145
146pub static ECHO_EVENT: LazyLock<EventType> = LazyLock::new(|| {
147 let mut params = common_params();
148 params.extend([
149 p("src_ip", "string", "The pinging host's IP", true),
150 p("dst_ip", "string", "The IP being pinged", true),
151 p("identifier", "number", "The echo identifier", true),
152 p("sequence", "number", "The echo sequence number", true),
153 p(
154 "data",
155 "string",
156 "The echo payload (text, or hex per encoding)",
157 true,
158 ),
159 p(
160 "encoding",
161 "string",
162 "utf8 or hex, as data is written",
163 true,
164 ),
165 ]);
166 EventType::new(
167 "vxlan_icmp_echo_request",
168 "A host on the overlay pings dst_ip. Answer vxlan_icmp_echo_reply if that host is up, or nothing.",
169 echo_reply_action().example.clone(),
170 )
171 .with_parameters(params)
172 .with_actions(vec![echo_reply_action()])
173});
174
175pub static UDP_EVENT: LazyLock<EventType> = LazyLock::new(|| {
176 let mut params = common_params();
177 params.extend([
178 p("src_ip", "string", "The sending host's IP", true),
179 p("src_port", "number", "The sender's UDP port", true),
180 p("dst_ip", "string", "The IP it was sent to", true),
181 p("dst_port", "number", "The UDP port it was sent to", true),
182 p(
183 "data",
184 "string",
185 "The payload (text, or hex per encoding)",
186 true,
187 ),
188 p(
189 "encoding",
190 "string",
191 "utf8 or hex, as data is written",
192 true,
193 ),
194 ]);
195 EventType::new(
196 "vxlan_udp_datagram",
197 "A host on the overlay sent a UDP datagram to dst_ip:dst_port. Answer vxlan_udp_reply, or nothing.",
198 udp_reply_action().example.clone(),
199 )
200 .with_parameters(params)
201 .with_actions(vec![udp_reply_action()])
202});
203
204pub fn mac_param(v: &Value, default: Mac) -> Result<Mac> {
205 match v["mac"].as_str() {
206 Some(s) => frame::parse_mac(s),
207 None => Ok(default),
208 }
209}
210
211impl FrameContext {
212 fn our_mac(&self, ip: Option<Ipv4Addr>) -> Mac {
213 ip.and_then(|ip| {
214 self.macs
215 .lock()
216 .unwrap_or_else(|e| e.into_inner())
217 .get(&ip)
218 .copied()
219 })
220 .unwrap_or(self.default_mac)
221 }
222 fn wrap(&self, inner: Vec<u8>) -> ActionResult {
225 let header = frame::Header {
226 sequence: self.header.sequence.map(|_| 0),
227 ..self.header
228 };
229 ActionResult::Output(frame::encap(self.encap, &header, &inner))
230 }
231 fn ip_reply(&self, protocol: u8, payload: &[u8]) -> Result<ActionResult> {
233 let (src, dst) = (
234 self.frame.dst_ip.context("not an IPv4 request")?,
235 self.frame.src_ip.context("not an IPv4 request")?,
236 );
237 let packet = frame::ipv4(src, dst, protocol, rand::random(), payload);
238 Ok(self.wrap(frame::ethernet(
239 self.frame.src_mac,
240 self.our_mac(Some(src)),
241 frame::ETHERTYPE_IPV4,
242 &packet,
243 )))
244 }
245}
246
247impl Protocol for VxlanProtocol {
248 fn protocol_name(&self) -> &'static str {
249 "VXLAN"
250 }
251 fn stack_name(&self) -> &'static str {
252 "ETH>IP>UDP>VXLAN"
253 }
254 fn keywords(&self) -> Vec<&'static str> {
255 vec![
256 "vxlan",
257 "geneve",
258 "gre",
259 "gretap",
260 "gre-in-udp",
261 "overlay",
262 "vtep",
263 "4789",
264 "6081",
265 "4754",
266 "tunnel endpoint",
267 ]
268 }
269 fn description(&self) -> &'static str {
270 "VXLAN/Geneve/GRE (gretap in UDP) tunnel endpoint whose overlay hosts the model plays: answers ARP, ping and UDP arriving through the tunnel from Linux, Open vSwitch or a switch"
271 }
272 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
273 Vec::new()
274 }
275 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
276 vec![arp_reply_action(), echo_reply_action(), udp_reply_action()]
277 }
278 fn get_event_types(&self) -> Vec<EventType> {
279 vec![ARP_EVENT.clone(), ECHO_EVENT.clone(), UDP_EVENT.clone()]
280 }
281 fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
282 Some(crate::protocol::BindingDefaults::port_based("127.0.0.1", 0))
283 }
284 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
285 vec![
286 ParameterDefinition {
287 name: "encapsulation".into(),
288 type_hint: "string".into(),
289 description: "vxlan (RFC 7348, port 4789), geneve (RFC 8926, port 6081) or gretap (Ethernet over GRE, RFC 2784/2890, in UDP per RFC 8086 on port 4754: a Linux gretap link with encap fou). Pass the port for geneve and gretap; with none the server takes VXLAN's 4789".into(),
290 required: false,
291 example: json!("geneve"),
292 default: Some(json!(DEFAULT_ENCAPSULATION)),
293 },
294 ParameterDefinition {
295 name: "vni".into(),
296 type_hint: "number".into(),
297 description: "Serve only this VNI; for gretap, only frames carrying this GRE key, so keyless frames are dropped (default: every VNI or key that arrives, and frames without a key)".into(),
298 required: false,
299 example: json!(42),
300 default: None,
301 },
302 ParameterDefinition {
303 name: "overlay_mac".into(),
304 type_hint: "string".into(),
305 description: "The MAC of the overlay hosts when an ARP reply names none".into(),
306 required: false,
307 example: json!("02:4e:47:00:00:02"),
308 default: Some(json!(frame::DEFAULT_MAC)),
309 },
310 ]
311 }
312 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
313 use crate::protocol::metadata::*;
314 ProtocolMetadataV2::builder()
315 .state(DevelopmentState::Experimental)
316 .privilege_requirement(PrivilegeRequirement::None)
317 .well_known_udp_port(4789)
318 .connectionless()
319 .deliberately_silent()
320 .implementation("Hand-rolled VXLAN (RFC 7348), Geneve (RFC 8926, options skipped) and Ethernet-over-GRE (RFC 2784/2890 key, checksum and sequence number; carried in UDP per RFC 8086) decapsulation, with Ethernet, ARP, IPv4 (header checksum checked, fragments not reassembled), ICMP echo and UDP (checksums checked and written) for the overlay hosts; replies go back through the tunnel to the sending endpoint, mirroring the request's VNI or GRE key, checksum and sequence flags")
321 .llm_control("Which overlay IPs exist and at which MAC (ARP), which hosts answer ping, and what each UDP service replies")
322 .e2e_testing("tests/server/vxlan: the Linux kernel's vxlan driver, and its gretap driver over a fou port (GRE key, checksum and sequence numbers on), each in a network namespace (ping and a UDP exchange through the tunnel, the neighbour entry the model's ARP reply created); the pcap oracle (tshark's vxlan, geneve and gre dissectors) over NetGet's frames; GRE refusals (version, protocol type, checksum, key, truncation) before the model")
323 .notes("DELIBERATELY SILENT: an overlay IP with no host is silence, never a reply, because every ARP, echo or UDP reply asserts that a host exists (logged decision=model_silent / fail_closed_*). No TCP, no IPv6 (frames are logged and dropped), no IP fragmentation, no MAC learning or flooding to other VTEPs. GRE is GRE-in-UDP only: GRE directly on IP protocol 47 needs a raw socket and is not implemented. Geneve interop with a kernel peer is unverified here; its framing is checked by tshark.")
324 .max_inbound_bytes(frame::MAX_DATAGRAM)
325 .build()
326 }
327 fn example_prompt(&self) -> &'static str {
328 "Be a VXLAN endpoint on VNI 42 where 10.99.0.2 answers ping and echoes UDP"
329 }
330 fn get_startup_examples(&self) -> StartupExamples {
331 let base = json!({"type":"open_server","base_stack":"vxlan","port":0,"startup_params":{"vni":42},
332 "instruction":"You are host 10.99.0.2 (MAC 02:4e:47:00:00:02) on VNI 42. Answer ARP for 10.99.0.2, answer pings to it, and echo UDP back."});
333 let mut static_example = base.clone();
334 static_example["event_handlers"] = json!([
335 {"event_pattern":"vxlan_arp_request","handler":{"type":"static","actions":[arp_reply_action().example]}},
336 {"event_pattern":"vxlan_icmp_echo_request","handler":{"type":"static","actions":[echo_reply_action().example]}}
337 ]);
338 let mut scripted = base.clone();
339 scripted["event_handlers"] = json!([{"event_pattern":"*","handler":{"type":"script","language":"python",
340 "code":"import json,sys\ni=json.load(sys.stdin); t=i['event_type_id']; e=i['event']\na=[]\nif t=='vxlan_arp_request' and e['target_ip']=='10.99.0.2': a=[{'type':'vxlan_arp_reply','mac':'02:4e:47:00:00:02'}]\nelif t=='vxlan_icmp_echo_request' and e['dst_ip']=='10.99.0.2': a=[{'type':'vxlan_icmp_echo_reply'}]\nelif t=='vxlan_udp_datagram': a=[{'type':'vxlan_udp_reply','data':e['data'],'encoding':e['encoding']}]\nprint(json.dumps({'actions':a}))"}}]);
341 StartupExamples::new(base, scripted, static_example)
342 }
343 fn group_name(&self) -> &'static str {
344 "Network"
345 }
346}
347
348impl Server for VxlanProtocol {
349 fn spawn(
350 &self,
351 ctx: SpawnContext,
352 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
353 {
354 Box::pin(super::spawn(ctx))
355 }
356 fn execute_action(&self, action: Value) -> Result<ActionResult> {
357 let kind = action["type"].as_str().unwrap_or_default();
358 match kind {
360 "vxlan_arp_reply" => {
361 mac_param(&action, [2, 0, 0, 0, 0, 1])?;
362 }
363 "vxlan_icmp_echo_reply" => {}
364 "vxlan_udp_reply" => {
365 frame::data_bytes(&action)?;
366 }
367 other => bail!("Unknown VXLAN action {other:?}"),
368 }
369 let Some(ctx) = &self.request else {
370 return Ok(ActionResult::NoAction);
371 };
372 match (kind, &ctx.frame.payload) {
373 ("vxlan_arp_reply", Payload::Arp(arp)) if arp.request => {
374 let mac = mac_param(&action, ctx.default_mac)?;
375 ctx.macs
376 .lock()
377 .unwrap_or_else(|e| e.into_inner())
378 .insert(arp.target_ip, mac);
379 Ok(ctx.wrap(frame::arp_frame(
380 false,
381 mac,
382 arp.target_ip,
383 arp.sender_mac,
384 arp.sender_ip,
385 )))
386 }
387 (
388 "vxlan_icmp_echo_reply",
389 Payload::Echo {
390 request: true,
391 identifier,
392 sequence,
393 data,
394 },
395 ) => ctx.ip_reply(1, &frame::icmp_echo(false, *identifier, *sequence, data)),
396 (
397 "vxlan_udp_reply",
398 Payload::Udp {
399 src_port, dst_port, ..
400 },
401 ) => {
402 let data = frame::data_bytes(&action)?;
403 let (src, dst) = (
404 ctx.frame.dst_ip.context("not an IPv4 request")?,
405 ctx.frame.src_ip.context("not an IPv4 request")?,
406 );
407 ctx.ip_reply(17, &frame::udp(src, dst, *dst_port, *src_port, &data))
408 }
409 (kind, _) => bail!("{kind} does not answer this frame"),
410 }
411 }
412}