1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, ParameterDefinition, StartupExamples,
7};
8use crate::protocol::{ConnectContext, EventType};
9use crate::server::vxlan::actions::{action, data_params, p};
10use crate::server::vxlan::frame;
11use crate::state::app_state::AppState;
12use anyhow::{bail, ensure, Context, Result};
13use serde_json::{json, Value};
14use std::net::Ipv4Addr;
15use std::sync::LazyLock;
16
17pub const DEFAULT_VNI: u32 = 1;
18pub const DEFAULT_GRE_CHECKSUM: bool = false;
20pub const DEFAULT_GRE_SEQUENCE: bool = false;
22
23#[derive(Default)]
24pub struct VxlanClientProtocol;
25impl VxlanClientProtocol {
26 pub fn new() -> Self {
27 Self
28 }
29}
30
31fn resolve_action() -> ActionDefinition {
32 action(
33 "vxlan_resolve",
34 "ARP for an overlay IP; the answer arrives as vxlan_arp_resolved (or vxlan_unreachable).",
35 vec![p(
36 "ip",
37 "string",
38 "The overlay IPv4 address to resolve",
39 true,
40 )],
41 json!({"type":"vxlan_resolve","ip":"10.99.0.1"}),
42 )
43}
44
45fn ping_action() -> ActionDefinition {
46 action(
47 "vxlan_ping",
48 "Send one ICMP echo request through the tunnel (ARPing first if needed); the reply arrives as vxlan_icmp_echo_reply.",
49 vec![
50 p("ip", "string", "The overlay IPv4 address to ping", true),
51 p("data", "string", "Echo payload text (default: netget)", false),
52 ],
53 json!({"type":"vxlan_ping","ip":"10.99.0.1","data":"netget"}),
54 )
55}
56
57fn send_udp_action() -> ActionDefinition {
58 let mut params = vec![
59 p("ip", "string", "The overlay IPv4 address to send to", true),
60 p("port", "number", "The destination UDP port", true),
61 p(
62 "source_port",
63 "number",
64 "The source port (default 40000)",
65 false,
66 ),
67 ];
68 params.extend(data_params());
69 action(
70 "vxlan_send_udp",
71 "Send a UDP datagram through the tunnel (ARPing first if needed). Replies arrive as vxlan_udp_datagram; a closed port as vxlan_icmp_error.",
72 params,
73 json!({"type":"vxlan_send_udp","ip":"10.99.0.1","port":9999,"data":"hello","encoding":"utf8"}),
74 )
75}
76
77fn disconnect_action() -> ActionDefinition {
78 action(
79 "disconnect",
80 "Close the tunnel endpoint.",
81 vec![],
82 json!({"type":"disconnect"}),
83 )
84}
85
86pub fn actions() -> Vec<ActionDefinition> {
87 vec![
88 resolve_action(),
89 ping_action(),
90 send_udp_action(),
91 disconnect_action(),
92 ]
93}
94
95fn ev(id: &str, description: &str, params: Vec<crate::llm::actions::Parameter>) -> EventType {
96 EventType::new(id, description, ping_action().example.clone())
97 .with_parameters(params)
98 .with_actions(actions())
99}
100
101pub static READY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
102 ev(
103 "vxlan_ready",
104 "The tunnel endpoint is up and this host is on the overlay.",
105 vec![
106 p("overlay_ip", "string", "This host's overlay IP", true),
107 p("overlay_mac", "string", "This host's overlay MAC", true),
108 p(
109 "remote_vtep",
110 "string",
111 "The tunnel endpoint frames go to",
112 true,
113 ),
114 p(
115 "vni",
116 "number",
117 "The overlay network identifier, or the GRE key (null for gretap without a key)",
118 false,
119 ),
120 p("encapsulation", "string", "vxlan, geneve or gretap", true),
121 ],
122 )
123});
124
125pub static RESOLVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
126 ev(
127 "vxlan_arp_resolved",
128 "An ARP request you asked for was answered.",
129 vec![
130 p("ip", "string", "The overlay IPv4 address", true),
131 p("mac", "string", "The MAC that answered for it", true),
132 ],
133 )
134});
135
136pub static UNREACHABLE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
137 ev(
138 "vxlan_unreachable",
139 "Nobody answered ARP for an IP, so what was meant for it was not sent.",
140 vec![
141 p("ip", "string", "The overlay IP nobody answered for", true),
142 p(
143 "action",
144 "string",
145 "The action that was waiting on it",
146 true,
147 ),
148 ],
149 )
150});
151
152pub static ECHO_REPLY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
153 ev(
154 "vxlan_icmp_echo_reply",
155 "A ping was answered.",
156 vec![
157 p("src_ip", "string", "The overlay IP that answered", true),
158 p("identifier", "number", "The echo identifier", true),
159 p("sequence", "number", "The echo sequence number", true),
160 p("data", "string", "The echoed payload", true),
161 p(
162 "encoding",
163 "string",
164 "utf8 or hex, as data is written",
165 true,
166 ),
167 p(
168 "rtt_ms",
169 "number",
170 "Round-trip time in milliseconds, when the request was ours",
171 false,
172 ),
173 ],
174 )
175});
176
177pub static UDP_EVENT: LazyLock<EventType> = LazyLock::new(|| {
178 ev(
179 "vxlan_udp_datagram",
180 "A UDP datagram arrived for this host through the tunnel.",
181 vec![
182 p("src_ip", "string", "The sender's overlay IP", true),
183 p("src_port", "number", "The sender's port", true),
184 p("dst_port", "number", "The port it was sent to", true),
185 p("data", "string", "The datagram's payload", true),
186 p(
187 "encoding",
188 "string",
189 "utf8 or hex, as data is written",
190 true,
191 ),
192 ],
193 )
194});
195
196pub static ICMP_ERROR_EVENT: LazyLock<EventType> =
197 LazyLock::new(|| {
198 ev(
199 "vxlan_icmp_error",
200 "A host or router reported a delivery failure, e.g. port unreachable for a UDP datagram.",
201 vec![
202 p("from", "string", "Who reported it", true),
203 p("meaning", "string", "port_unreachable, host_unreachable, ttl_exceeded, ...", true),
204 p("type", "number", "The ICMP type number", true),
205 p("code", "number", "The ICMP code number", true),
206 p("original_dst", "string", "Where the failed packet was going", true),
207 p("original_dst_port", "number", "Its UDP destination port, if it was UDP", false),
208 ],
209 )
210 });
211
212pub fn ip_of(v: &Value) -> Result<Ipv4Addr> {
213 v["ip"]
214 .as_str()
215 .context("ip is required")?
216 .parse()
217 .context("ip is an IPv4 address")
218}
219
220fn port_of(v: &Value, key: &str) -> Result<Option<u16>> {
221 match v.get(key) {
222 None | Some(Value::Null) => Ok(None),
223 Some(x) => {
224 let n = x.as_u64().filter(|n| (1..=65535).contains(n));
225 Ok(Some(n.with_context(|| format!("{key} is 1-65535"))? as u16))
226 }
227 }
228}
229
230pub fn check(v: &Value) -> Result<(Ipv4Addr, Option<u16>, Option<u16>)> {
232 let ip = ip_of(v)?;
233 match v["type"].as_str().unwrap_or_default() {
234 "vxlan_resolve" => Ok((ip, None, None)),
235 "vxlan_ping" => {
236 if let Some(d) = v.get("data").and_then(Value::as_str) {
237 ensure!(d.len() <= frame::MAX_UDP_PAYLOAD, "ping data is too long");
238 }
239 Ok((ip, None, None))
240 }
241 "vxlan_send_udp" => {
242 frame::data_bytes(v)?;
243 let port = port_of(v, "port")?.context("port is required")?;
244 Ok((ip, Some(port), port_of(v, "source_port")?))
245 }
246 other => bail!("Unknown VXLAN client action {other:?}"),
247 }
248}
249
250impl Protocol for VxlanClientProtocol {
251 fn protocol_name(&self) -> &'static str {
252 "VXLAN"
253 }
254 fn stack_name(&self) -> &'static str {
255 "ETH>IP>UDP>VXLAN"
256 }
257 fn keywords(&self) -> Vec<&'static str> {
258 vec![
259 "vxlan",
260 "geneve",
261 "gre",
262 "gretap",
263 "overlay",
264 "vtep",
265 "tunnel",
266 "vxlan client",
267 ]
268 }
269 fn description(&self) -> &'static str {
270 "VXLAN/Geneve/GRE (gretap in UDP) tunnel endpoint as a host on the overlay: ARPs, pings and sends UDP through the tunnel to a remote endpoint (Linux, Open vSwitch, a switch), and answers ARP and ping for itself"
271 }
272 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
273 actions()
274 }
275 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
276 vec![]
277 }
278 fn get_event_types(&self) -> Vec<EventType> {
279 vec![
280 READY_EVENT.clone(),
281 RESOLVED_EVENT.clone(),
282 UNREACHABLE_EVENT.clone(),
283 ECHO_REPLY_EVENT.clone(),
284 UDP_EVENT.clone(),
285 ICMP_ERROR_EVENT.clone(),
286 ]
287 }
288 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
289 vec![
290 ParameterDefinition {
291 name: "overlay_ip".into(),
292 type_hint: "string".into(),
293 description: "This host's IPv4 address on the overlay".into(),
294 required: true,
295 example: json!("10.99.0.2"),
296 default: None,
297 },
298 ParameterDefinition {
299 name: "overlay_mac".into(),
300 type_hint: "string".into(),
301 description: "This host's MAC on the overlay".into(),
302 required: false,
303 example: json!("02:4e:47:00:00:02"),
304 default: Some(json!(frame::DEFAULT_MAC)),
305 },
306 ParameterDefinition {
307 name: "vni".into(),
308 type_hint: "number".into(),
309 description: "The overlay network identifier (0-16777215). For gretap it is the GRE key (0-4294967295), and leaving it out sends and accepts frames with no key".into(),
310 required: false,
311 example: json!(42),
312 default: Some(json!(DEFAULT_VNI)),
313 },
314 ParameterDefinition {
315 name: "encapsulation".into(),
316 type_hint: "string".into(),
317 description: "vxlan (port 4789), geneve (port 6081) or gretap (Ethernet over GRE in UDP, port 4754: a Linux gretap link with encap fou)".into(),
318 required: false,
319 example: json!("vxlan"),
320 default: Some(json!(crate::server::vxlan::actions::DEFAULT_ENCAPSULATION)),
321 },
322 ParameterDefinition {
323 name: "gre_checksum".into(),
324 type_hint: "boolean".into(),
325 description: "gretap only: put a GRE checksum on every frame (what a Linux gretap with csum/ocsum expects)".into(),
326 required: false,
327 example: json!(true),
328 default: Some(json!(DEFAULT_GRE_CHECKSUM)),
329 },
330 ParameterDefinition {
331 name: "gre_sequence".into(),
332 type_hint: "boolean".into(),
333 description: "gretap only: number every frame with a GRE sequence number (what a Linux gretap with seq/iseq expects)".into(),
334 required: false,
335 example: json!(true),
336 default: Some(json!(DEFAULT_GRE_SEQUENCE)),
337 },
338 ParameterDefinition {
339 name: "local_address".into(),
340 type_hint: "string".into(),
341 description: "The underlay IP to send from and listen on: the remote VTEP's configured peer (default: the source of the route to it)".into(),
342 required: false,
343 example: json!("198.18.0.1"),
344 default: None,
345 },
346 ]
347 }
348 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
349 use crate::protocol::metadata::*;
350 ProtocolMetadataV2::builder()
351 .state(DevelopmentState::Experimental)
352 .privilege_requirement(PrivilegeRequirement::None)
353 .implementation("The server's VXLAN/Geneve/GRE-in-UDP and Ethernet/ARP/IPv4/ICMP/UDP framing (src/server/vxlan/frame.rs); one overlay host with an ARP cache, answering ARP and echo requests for its own IP in Rust; sends wait on ARP (2 s) and report vxlan_unreachable when nobody answers; for gretap, the GRE key, checksum and sequence number are each optional")
354 .llm_control("Which overlay IPs to resolve, ping and send UDP to, and what to do with what comes back")
355 .e2e_testing("tests/client/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, which answers ARP and ping itself, with a UDP service behind it and a closed port for the ICMP error")
356 .notes("No TCP, no IPv6, no IP fragmentation; the tunnel goes to one remote endpoint (no flooding or learning of others). GRE is GRE-in-UDP only: GRE directly on IP protocol 47 needs a raw socket and is not implemented.")
357 .max_inbound_bytes(frame::MAX_DATAGRAM)
358 .build()
359 }
360 fn example_prompt(&self) -> &'static str {
361 "Join VNI 42 through the VTEP at 198.18.0.2 as 10.99.0.2 and ping 10.99.0.1"
362 }
363 fn get_startup_examples(&self) -> StartupExamples {
364 let llm = json!({"type":"open_client","protocol":"vxlan","remote_addr":"127.0.0.1:4789",
365 "startup_params":{"overlay_ip":"10.99.0.2","vni":42},
366 "instruction":"Ping 10.99.0.1 through the tunnel and report the round-trip time"});
367 let mut static_example = llm.clone();
368 static_example["event_handlers"] = json!([
369 {"event_pattern":"vxlan_ready","handler":{"type":"static","actions":[ping_action().example]}},
370 {"event_pattern":"*","handler":{"type":"static","actions":[]}}
371 ]);
372 let mut scripted = static_example.clone();
373 scripted["event_handlers"][1] = json!({"event_pattern":"vxlan_icmp_echo_reply","handler":{"type":"script","language":"python",
374 "code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'vxlan_send_udp','ip':e['src_ip'],'port':9999,'data':'rtt %s ms' % e.get('rtt_ms')}]}))"}});
375 StartupExamples::new(llm, scripted, static_example)
376 }
377 fn group_name(&self) -> &'static str {
378 "Network"
379 }
380}
381
382impl Client for VxlanClientProtocol {
383 fn connect(
384 &self,
385 ctx: ConnectContext,
386 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
387 {
388 Box::pin(super::connect(ctx))
389 }
390 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
391 if v["type"] == "disconnect" {
392 return Ok(ClientActionResult::Disconnect);
393 }
394 check(&v)?;
395 Ok(ClientActionResult::Custom {
396 name: v["type"].as_str().unwrap_or_default().to_string(),
397 data: v,
398 })
399 }
400}