Skip to main content

netget/client/vxlan/
actions.rs

1//! What the model can do as a host on a VXLAN, Geneve or GRE (gretap) overlay, reached
2//! through a tunnel to one remote endpoint: resolve an IP, ping it, send it UDP.
3use 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;
18/// gretap: frames carry no GRE checksum unless asked.
19pub const DEFAULT_GRE_CHECKSUM: bool = false;
20/// gretap: frames carry no GRE sequence number unless asked.
21pub 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
230/// Validate a client action. Returns (destination, dst port, src port) where they apply.
231pub 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}