Skip to main content

netget/server/sunrpc/
actions.rs

1//! What the model decides as a portmapper: which programs are registered where (it answers
2//! every lookup and dump with mappings) and which registrations to accept. Rust owns XDR,
3//! record marking, the RPC error replies, NULL and GETTIME, and picks from the model's
4//! mappings the ones each procedure returns.
5use super::wire;
6use crate::llm::actions::{
7    protocol_trait::{ActionResult, Protocol, Server},
8    ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
9};
10use crate::protocol::{log_template::LogTemplate, EventType, SpawnContext};
11use crate::state::app_state::AppState;
12use anyhow::{bail, ensure, Context, Result};
13use serde_json::{json, Value};
14use std::sync::LazyLock;
15
16pub const MAPPINGS: &str = "sunrpc_mappings";
17pub const ACCEPT: &str = "sunrpc_accept";
18pub const REJECT: &str = "sunrpc_reject";
19
20#[derive(Default, Clone)]
21pub struct SunRpcProtocol;
22
23impl SunRpcProtocol {
24    pub fn new() -> Self {
25        Self
26    }
27}
28
29pub fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
30    Parameter {
31        name: name.into(),
32        type_hint: type_hint.into(),
33        description: description.into(),
34        required,
35    }
36}
37
38fn action(
39    name: &str,
40    description: &str,
41    parameters: Vec<Parameter>,
42    example: Value,
43    info: &str,
44) -> ActionDefinition {
45    ActionDefinition {
46        name: name.into(),
47        description: description.into(),
48        parameters,
49        example,
50        log_template: Some(LogTemplate::new().with_info(info)),
51    }
52}
53
54pub fn mappings_action() -> ActionDefinition {
55    action(
56        MAPPINGS,
57        "Answer a lookup or dump with the registered programs. Give every mapping you hold (or just the matching ones): NetGet picks what the procedure returns. An empty list means nothing is registered.",
58        vec![p(
59            "mappings",
60            "array",
61            "Each {program, version, protocol (tcp, udp, tcp6 or udp6), port, owner?}, e.g. NFS 100003 v3 on tcp port 2049",
62            true,
63        )],
64        json!({"type": MAPPINGS, "mappings": [
65            {"program": 100003, "version": 3, "protocol": "tcp", "port": 2049},
66            {"program": 100005, "version": 3, "protocol": "udp", "port": 20048}
67        ]}),
68        "-> portmapper {mappings}",
69    )
70}
71
72pub fn accept_action() -> ActionDefinition {
73    action(
74        ACCEPT,
75        "Accept the registration or removal (the caller is told it succeeded).",
76        vec![],
77        json!({"type": ACCEPT}),
78        "-> portmapper accept",
79    )
80}
81
82pub fn reject_action() -> ActionDefinition {
83    action(
84        REJECT,
85        "Refuse the registration or removal (the caller is told it failed).",
86        vec![],
87        json!({"type": REJECT}),
88        "-> portmapper reject",
89    )
90}
91
92fn common() -> Vec<Parameter> {
93    vec![
94        p(
95            "transport",
96            "string",
97            "tcp or udp: how the call arrived",
98            true,
99        ),
100        p(
101            "rpc_version",
102            "number",
103            "The portmapper version asked: 2 (PMAP), 3 or 4 (RPCBIND)",
104            true,
105        ),
106    ]
107}
108
109pub static QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
110    let mut params = common();
111    params.extend([
112        p(
113            "procedure",
114            "string",
115            "dump, getport, getaddr, getversaddr or getaddrlist",
116            true,
117        ),
118        p(
119            "program",
120            "number",
121            "The program looked up (absent for dump)",
122            false,
123        ),
124        p(
125            "program_name",
126            "string",
127            "Its well-known name, when it has one (nfs, mountd, …)",
128            false,
129        ),
130        p("program_version", "number", "The version looked up", false),
131        p(
132            "protocol",
133            "string",
134            "The transport looked up: tcp, udp, tcp6 or udp6",
135            false,
136        ),
137    ]);
138    EventType::new(
139        "sunrpc_query",
140        "A client asks which programs are registered (dump) or where one program is (getport/getaddr). Answer sunrpc_mappings.",
141        mappings_action().example.clone(),
142    )
143    .with_parameters(params)
144    .with_actions(vec![mappings_action()])
145});
146
147pub static REGISTER_EVENT: LazyLock<EventType> = LazyLock::new(|| {
148    let mut params = common();
149    params.extend([
150        p(
151            "operation",
152            "string",
153            "set (register) or unset (remove)",
154            true,
155        ),
156        p(
157            "program",
158            "number",
159            "The program being registered or removed",
160            true,
161        ),
162        p(
163            "program_name",
164            "string",
165            "Its well-known name, when it has one",
166            false,
167        ),
168        p(
169            "program_version",
170            "number",
171            "The version of the program being registered",
172            true,
173        ),
174        p(
175            "protocol",
176            "string",
177            "tcp, udp, tcp6 or udp6 (empty on an unset: every transport)",
178            true,
179        ),
180        p("port", "number", "The port it listens on (set only)", false),
181        p(
182            "owner",
183            "string",
184            "The owner the caller claims (RPCBIND only)",
185            false,
186        ),
187        p(
188            "credentials",
189            "object",
190            "The call's credentials: {flavor: none} or AUTH_SYS {uid, gid, machine}",
191            true,
192        ),
193    ]);
194    EventType::new(
195        "sunrpc_register",
196        "A program registers itself (set) or is removed (unset). Accept or reject; remember what you accept, since you answer later lookups.",
197        json!({"type": ACCEPT}),
198    )
199    .with_parameters(params)
200    .with_actions(vec![accept_action(), reject_action()])
201});
202
203pub fn check(v: &Value) -> Result<()> {
204    match v["type"].as_str().unwrap_or_default() {
205        MAPPINGS => {
206            let list = v["mappings"]
207                .as_array()
208                .context("mappings must be an array")?;
209            ensure!(
210                list.len() <= wire::MAX_MAPPINGS,
211                "at most {} mappings",
212                wire::MAX_MAPPINGS
213            );
214            for m in list {
215                wire::mapping_from_json(m)?;
216            }
217            Ok(())
218        }
219        ACCEPT | REJECT => Ok(()),
220        other => bail!("Unknown portmapper action {other:?}"),
221    }
222}
223
224impl Protocol for SunRpcProtocol {
225    fn protocol_name(&self) -> &'static str {
226        "SunRPC"
227    }
228    fn stack_name(&self) -> &'static str {
229        "ETH>IP>TCP|UDP>ONC-RPC>Portmapper"
230    }
231    fn keywords(&self) -> Vec<&'static str> {
232        vec![
233            "sunrpc",
234            "onc rpc",
235            "portmapper",
236            "portmap",
237            "rpcbind",
238            "rpcinfo",
239            "111",
240        ]
241    }
242    fn description(&self) -> &'static str {
243        "ONC RPC portmapper/rpcbind (PMAP v2, RPCBIND v3 and v4) on TCP and UDP: tells clients where RPC programs such as NFS and mountd listen"
244    }
245    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
246        Vec::new()
247    }
248    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
249        vec![mappings_action(), accept_action(), reject_action()]
250    }
251    fn get_event_types(&self) -> Vec<EventType> {
252        vec![QUERY_EVENT.clone(), REGISTER_EVENT.clone()]
253    }
254    fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
255        Some(crate::protocol::BindingDefaults::port_based("127.0.0.1", 0))
256    }
257    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
258        Vec::new()
259    }
260    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
261        use crate::protocol::metadata::*;
262        ProtocolMetadataV2::builder()
263            .state(DevelopmentState::Experimental)
264            .privilege_requirement(PrivilegeRequirement::PrivilegedPort(111))
265            .well_known_port(111)
266            .implementation("Hand-rolled ONC RPC (src/server/sunrpc/wire.rs): calls and replies with AUTH_NONE/AUTH_SYS, TCP record marking and UDP datagrams on the same port; PMAP v2 NULL/SET/UNSET/GETPORT/DUMP and RPCBIND v3/v4 NULL/SET/UNSET/GETADDR/DUMP/GETTIME/GETVERSADDR/GETADDRLIST; RPC_MISMATCH, PROG_UNAVAIL, PROG_MISMATCH, PROC_UNAVAIL and GARBAGE_ARGS answered in Rust")
267            .llm_control("Every lookup and dump (the mappings it answers with) and every registration and removal (accept or reject)")
268            .e2e_testing("tests/server/sunrpc: the stock libtirpc rpcinfo (-p, -s, -l, -T tcp|udp, -a) in a network namespace whose port 111 is DNATed to NetGet; raw XDR for the error replies and bounds")
269            .notes("Only the portmapper program (100000) is served; calls to any other program are PROG_UNAVAIL. CALLIT/INDIRECT and the statistics procedures are PROC_UNAVAIL. NetGet stores no registrations: the model holds them and answers lookups. Records are capped at 256 KiB in 64 fragments. A model failure answers SYSTEM_ERR.")
270            .answers_on_failure()
271            .request_only("Every reply answers an RPC call")
272            .max_inbound_bytes(wire::MAX_RECORD)
273            .build()
274    }
275    fn example_prompt(&self) -> &'static str {
276        "Portmapper on port 111 that says NFS v3 is on TCP 2049 and mountd v3 on UDP 20048"
277    }
278    fn get_startup_examples(&self) -> StartupExamples {
279        let base = json!({"type":"open_server","base_stack":"sunrpc","port":0,
280            "instruction":"Say NFS 100003 v3 is on tcp 2049 and mountd 100005 v3 on udp 20048; accept every registration and include it afterwards"});
281        let mut static_example = base.clone();
282        static_example["event_handlers"] = json!([
283            {"event_pattern":"sunrpc_query","handler":{"type":"static","actions":[mappings_action().example]}},
284            {"event_pattern":"sunrpc_register","handler":{"type":"static","actions":[{"type":REJECT}]}}
285        ]);
286        let mut scripted = base.clone();
287        scripted["event_handlers"] = json!([{"event_pattern":"*","handler":{"type":"script","language":"python",
288            "code":"import json,sys\ni=json.load(sys.stdin)\nif i['event_type_id']=='sunrpc_register':\n  a=[{'type':'sunrpc_accept'}]\nelse:\n  a=[{'type':'sunrpc_mappings','mappings':[{'program':100003,'version':3,'protocol':'tcp','port':2049}]}]\nprint(json.dumps({'actions':a}))"}}]);
289        StartupExamples::new(base, scripted, static_example)
290    }
291    fn group_name(&self) -> &'static str {
292        "Network Services"
293    }
294}
295
296impl Server for SunRpcProtocol {
297    fn spawn(
298        &self,
299        ctx: SpawnContext,
300    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
301    {
302        Box::pin(super::spawn(ctx))
303    }
304    fn execute_action(&self, action: Value) -> Result<ActionResult> {
305        check(&action)?;
306        let name = action["type"].as_str().unwrap_or_default().to_string();
307        Ok(ActionResult::Custom { name, data: action })
308    }
309}