Skip to main content

netget/server/vxlan/
actions.rs

1//! What the model answers as hosts on a VXLAN, Geneve or GRE (gretap) overlay: who has an IP (ARP), echo
2//! replies, and UDP replies. The frames are built here from the request they answer, so the
3//! model supplies only what a host would decide — its MAC, and what its service says.
4use 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/// The frame an answer answers, and what the server knows about its overlay hosts.
20#[derive(Clone)]
21pub struct FrameContext {
22    pub encap: Encap,
23    /// The request's encapsulation header, which the reply mirrors: the same VNI or GRE key,
24    /// and for GRE a checksum and a sequence number when the request had them.
25    pub header: frame::Header,
26    pub frame: Frame,
27    pub default_mac: Mac,
28    /// The MAC each IP was given in an ARP reply, used as the source of that IP's frames.
29    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    /// Encapsulated as the request was. A GRE sequence number is a placeholder here; the
223    /// server writes the real one as the reply leaves.
224    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    /// An IPv4 reply to the request's sender, from the address it was sent to.
232    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        // Validate first, so the registry's instance (no request) checks what it can.
359        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}