Skip to main content

netget/server/ethernet_ip/
codec.rs

1//! EtherNet/IP encapsulation, CPF and selected explicit CIP object services.
2use crate::server::ics_support::{self as io, number, DeviceSession, ScannerSession};
3use anyhow::{bail, ensure, Context, Result};
4use serde_json::{json, Value};
5use tokio::{io::AsyncWriteExt, net::TcpStream};
6pub const MAX_FRAME: usize = 4096;
7pub const PORT: u16 = 44818;
8fn u16le(b: &[u8]) -> u16 {
9    u16::from_le_bytes([b[0], b[1]])
10}
11fn u32le(b: &[u8]) -> u32 {
12    u32::from_le_bytes([b[0], b[1], b[2], b[3]])
13}
14pub fn encapsulate(
15    command: u16,
16    session: u32,
17    context: [u8; 8],
18    status: u32,
19    data: &[u8],
20) -> Vec<u8> {
21    let mut p = vec![];
22    p.extend(command.to_le_bytes());
23    p.extend(
24        u16::try_from(data.len())
25            .expect("bounded payload")
26            .to_le_bytes(),
27    );
28    p.extend(session.to_le_bytes());
29    p.extend(status.to_le_bytes());
30    p.extend(context);
31    p.extend([0; 4]);
32    p.extend(data);
33    p
34}
35pub async fn read(s: &mut TcpStream) -> Result<Vec<u8>> {
36    io::frame(s, 24, MAX_FRAME, |h| {
37        ensure!(h[20..24] == [0; 4], "unsupported encapsulation options");
38        Ok(24 + u16le(&h[2..]) as usize)
39    })
40    .await
41}
42fn context(f: &[u8]) -> [u8; 8] {
43    f[12..20].try_into().expect("checked header")
44}
45/// Identity data uses an explicit NetGet simulator identity, rather than a vendor impersonation.
46pub fn identity(addr: std::net::SocketAddr) -> Vec<u8> {
47    let mut data = vec![1, 0];
48    data.extend(2u16.to_be_bytes());
49    data.extend(addr.port().to_be_bytes());
50    data.extend(match addr.ip() {
51        std::net::IpAddr::V4(v) => v.octets(),
52        _ => [127, 0, 0, 1],
53    });
54    data.extend([0; 8]);
55    data.extend(0u16.to_le_bytes());
56    data.extend(0u16.to_le_bytes());
57    data.extend(1u16.to_le_bytes());
58    data.extend([1, 0]);
59    data.extend(0u16.to_le_bytes());
60    data.extend(1u32.to_le_bytes());
61    let name = b"NetGet simulator";
62    data.push(name.len() as u8);
63    data.extend(name);
64    data.push(3);
65    let mut out = vec![1, 0, 0x0c, 0];
66    out.extend((data.len() as u16).to_le_bytes());
67    out.extend(data);
68    out
69}
70fn cpf(cip: &[u8]) -> Vec<u8> {
71    let mut p = vec![0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0xb2, 0];
72    p.extend((cip.len() as u16).to_le_bytes());
73    p.extend(cip);
74    p
75}
76fn cip_payload(p: &[u8]) -> Result<&[u8]> {
77    ensure!(
78        p.len() >= 16
79            && p[..4] == [0; 4]
80            && u16le(&p[6..]) == 2
81            && p[8..12] == [0; 4]
82            && p[12..14] == [0xb2, 0],
83        "unsupported CPF/interface"
84    );
85    let n = u16le(&p[14..]) as usize;
86    ensure!(p.len() == 16 + n, "CPF length mismatch");
87    Ok(&p[16..])
88}
89fn path(cip: &[u8]) -> Result<(u16, u16, Option<u16>, &[u8])> {
90    ensure!(cip.len() >= 2, "short CIP request");
91    let end = 2 + cip[1] as usize * 2;
92    ensure!(end <= cip.len(), "short CIP path");
93    let mut pos = 2;
94    let mut c = None;
95    let mut i = None;
96    let mut a = None;
97    while pos < end {
98        let code = cip[pos];
99        let (n, used) = match code {
100            0x20 | 0x24 | 0x30 => {
101                ensure!(pos + 2 <= end, "short logical segment");
102                (cip[pos + 1] as u16, 2)
103            }
104            0x21 | 0x25 | 0x31 => {
105                ensure!(
106                    pos + 4 <= end && cip[pos + 1] == 0,
107                    "short/padded logical segment"
108                );
109                (u16le(&cip[pos + 2..]), 4)
110            }
111            _ => bail!("unsupported CIP path segment"),
112        };
113        let slot = match code & 0xfc {
114            0x20 => &mut c,
115            0x24 => &mut i,
116            0x30 => &mut a,
117            _ => unreachable!(),
118        };
119        ensure!(slot.is_none(), "duplicate CIP path segment");
120        *slot = Some(n);
121        pos += used;
122    }
123    Ok((
124        c.context("class missing")?,
125        i.context("instance missing")?,
126        a,
127        &cip[end..],
128    ))
129}
130fn cip_error(service: u8, status: u8) -> Vec<u8> {
131    vec![service | 0x80, 0, status, 0]
132}
133fn encode_value(v: &Value) -> Result<Vec<u8>> {
134    let kind = v["value_type"].as_str().context("value_type required")?;
135    Ok(match kind {
136        "uint8" => vec![number(v, "value", 255)? as u8],
137        "uint16" => (number(v, "value", 65535)? as u16).to_le_bytes().to_vec(),
138        "uint32" => (number(v, "value", u32::MAX as u64)? as u32)
139            .to_le_bytes()
140            .to_vec(),
141        "int32" => {
142            let n = v["value"].as_i64().context("signed value")?;
143            i32::try_from(n)?.to_le_bytes().to_vec()
144        }
145        "real" => {
146            let n = v["value"].as_f64().context("real value")?;
147            ensure!(n.is_finite() && (n as f32).is_finite(), "nonfinite value");
148            (n as f32).to_le_bytes().to_vec()
149        }
150        "string" => {
151            let t = v["value"].as_str().context("string value")?;
152            ensure!(
153                t.is_ascii() && t.len() <= 255,
154                "short ASCII string required"
155            );
156            let mut b = vec![t.len() as u8];
157            b.extend(t.as_bytes());
158            b
159        }
160        _ => bail!("unsupported attribute value_type"),
161    })
162}
163fn decode_value(kind: &str, b: &[u8]) -> Result<Value> {
164    Ok(match kind {
165        "uint8" => {
166            ensure!(b.len() == 1, "uint8 length");
167            json!(b[0])
168        }
169        "uint16" => {
170            ensure!(b.len() == 2, "uint16 length");
171            json!(u16le(b))
172        }
173        "uint32" => {
174            ensure!(b.len() == 4, "uint32 length");
175            json!(u32le(b))
176        }
177        "int32" => {
178            ensure!(b.len() == 4, "int32 length");
179            json!(i32::from_le_bytes(b.try_into()?))
180        }
181        "real" => {
182            ensure!(b.len() == 4, "real length");
183            let n = f32::from_le_bytes(b.try_into()?);
184            ensure!(n.is_finite(), "nonfinite real");
185            json!(n)
186        }
187        "string" => {
188            ensure!(
189                !b.is_empty() && b.len() == 1 + b[0] as usize,
190                "short string length"
191            );
192            json!(std::str::from_utf8(&b[1..])?)
193        }
194        _ => bail!("unsupported attribute value_type"),
195    })
196}
197pub struct Device {
198    session: u32,
199    addr: std::net::SocketAddr,
200    pub types: Vec<Value>,
201    context: [u8; 8],
202}
203impl Default for Device {
204    fn default() -> Self {
205        Self {
206            session: 0,
207            types: vec![],
208            context: [0; 8],
209            addr: "127.0.0.1:44818".parse().expect("constant"),
210        }
211    }
212}
213#[async_trait::async_trait]
214impl DeviceSession for Device {
215    async fn read(&mut self, s: &mut TcpStream) -> Result<Vec<u8>> {
216        self.addr = s.local_addr()?;
217        read(s).await
218    }
219    fn receive(&mut self, f: &[u8]) -> Result<(Vec<u8>, Option<Value>)> {
220        ensure!(
221            f.len() >= 24 && u32le(&f[8..]) == 0,
222            "invalid encapsulation header"
223        );
224        let command = u16le(f);
225        let session = u32le(&f[4..]);
226        let payload = &f[24..];
227        let ctx = context(f);
228        let auto = |status, data: &[u8]| encapsulate(command, session, ctx, status, data);
229        if matches!(command, 0x63 | 0x64 | 0x04) {
230            ensure!(
231                payload.is_empty() && session == 0,
232                "discovery fields must be zero"
233            );
234            let data = match command {
235                0x63 => identity(self.addr),
236                0x64 => vec![0, 0],
237                _ => {
238                    let mut p = vec![1, 0, 0, 1, 20, 0, 1, 0, 0x20, 0];
239                    p.extend(b"Communications\0\0");
240                    p
241                }
242            };
243            return Ok((auto(0, &data), None));
244        }
245        if command == 0x65 {
246            if payload != [1, 0, 0, 0] || session != 0 {
247                return Ok((auto(0x69, &[]), None));
248            }
249            static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1);
250            self.session = NEXT
251                .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
252                .max(1);
253            return Ok((encapsulate(command, self.session, ctx, 0, payload), None));
254        }
255        if self.session == 0 || session != self.session {
256            return Ok((auto(0x64, &[]), None));
257        }
258        if command == 0x66 {
259            bail!("session unregistered");
260        }
261        if command != 0x6f {
262            return Ok((auto(1, &[]), None));
263        }
264        let cip = cip_payload(payload)?;
265        ensure!(!cip.is_empty(), "empty CIP");
266        let (c, i, a, body) = match path(cip) {
267            Ok(p) => p,
268            Err(_) => return Ok((auto(0, &cpf(&cip_error(cip[0], 4))), None)),
269        };
270        if !matches!(cip[0], 0x0e | 0x10) {
271            return Ok((auto(0, &cpf(&cip_error(cip[0], 8))), None));
272        }
273        let Some(attribute) = a else {
274            return Ok((auto(0, &cpf(&cip_error(cip[0], 4))), None));
275        };
276        if cip[0] == 0x0e {
277            ensure!(body.is_empty(), "unexpected GetAttributeSingle data");
278        }
279        let mut request = json!({"operation":if cip[0]==0x0e{"get"}else{"set"},"class":c,"instance":i,"attribute":attribute,"service":cip[0]});
280        // CIP carries no type tag: a declared schema supplies the attribute type.
281        self.context = ctx;
282        if cip[0] == 0x10 {
283            ensure!(body.len() <= 1024, "attribute too large");
284            let configured = self.types.iter().find(|a| {
285                a["class"] == c
286                    && a["instance"] == i
287                    && a["attribute"].as_u64() == Some(attribute as u64)
288            });
289            let kind = configured
290                .and_then(|t| t["value_type"].as_str())
291                .or(if c == 1 && i == 1 {
292                    match attribute {
293                        1..=5 => Some("uint16"),
294                        6 => Some("uint32"),
295                        7 => Some("string"),
296                        8 => Some("uint8"),
297                        _ => None,
298                    }
299                } else {
300                    None
301                });
302            let Some(kind) = kind else {
303                return Ok((auto(0, &cpf(&cip_error(cip[0], 0x14))), None));
304            };
305            let value = match decode_value(kind, body) {
306                Ok(v) => v,
307                Err(_) => return Ok((auto(0, &cpf(&cip_error(cip[0], 9))), None)),
308            };
309            request["value_type"] = json!(kind);
310            request["value"] = value;
311        }
312        Ok((vec![], Some(request)))
313    }
314    fn answer(&mut self, r: &Value, a: Option<&Value>) -> Result<Vec<u8>> {
315        let service = number(r, "service", 255)? as u8;
316        let a = a.filter(|a| a["type"] == "ethernet_ip_reply");
317        let mut cip = cip_error(service, 0x0f);
318        if let Some(a) = a {
319            if let Some(status) = a.get("status").filter(|v| !v.is_null()) {
320                let n = status.as_u64().context("status integer")?;
321                ensure!((1..=255).contains(&n), "error status 1..255");
322                cip = cip_error(service, n as u8);
323            } else if service == 0x0e {
324                match encode_value(a) {
325                    Ok(value) => {
326                        cip = cip_error(service, 0);
327                        cip.extend(value)
328                    }
329                    Err(_) => {}
330                }
331            } else if a["accepted"] == true {
332                cip = cip_error(service, 0);
333            }
334        }
335        Ok(encapsulate(0x6f, self.session, self.context, 0, &cpf(&cip)))
336    }
337}
338pub fn validate(a: &Value) -> Result<()> {
339    if a["type"] == "ethernet_ip_discover" {
340        return Ok(());
341    }
342    ensure!(
343        matches!(
344            a["type"].as_str(),
345            Some("ethernet_ip_get" | "ethernet_ip_set")
346        ),
347        "unknown explicit messaging action"
348    );
349    for field in ["class", "instance", "attribute"] {
350        number(a, field, 65535)?;
351    }
352    let kind = a["value_type"].as_str().context("value_type required")?;
353    ensure!(
354        matches!(
355            kind,
356            "uint8" | "uint16" | "uint32" | "int32" | "real" | "string"
357        ),
358        "unsupported value_type"
359    );
360    if a["type"] == "ethernet_ip_set" {
361        encode_value(a)?;
362    }
363    Ok(())
364}
365#[derive(Default)]
366pub struct Scanner {
367    session: u32,
368    context: u64,
369}
370impl Scanner {
371    async fn send(&mut self, s: &mut TcpStream, command: u16, payload: &[u8]) -> Result<Vec<u8>> {
372        self.context = self.context.wrapping_add(1);
373        let ctx = self.context.to_le_bytes();
374        let session = if command == 0x63 { 0 } else { self.session };
375        s.write_all(&encapsulate(command, session, ctx, 0, payload))
376            .await?;
377        let f = read(s).await?;
378        ensure!(
379            u16le(&f) == command && context(&f) == ctx,
380            "encapsulation response correlation failed"
381        );
382        ensure!(
383            u32le(&f[8..]) == 0,
384            "encapsulation error {}",
385            u32le(&f[8..])
386        );
387        if command != 0x65 {
388            ensure!(u32le(&f[4..]) == session, "session mismatch");
389        }
390        Ok(f)
391    }
392}
393#[async_trait::async_trait]
394impl ScannerSession for Scanner {
395    async fn open(&mut self, s: &mut TcpStream) -> Result<()> {
396        let f = self.send(s, 0x65, &[1, 0, 0, 0]).await?;
397        ensure!(f[24..] == [1, 0, 0, 0], "registration payload invalid");
398        self.session = u32le(&f[4..]);
399        ensure!(self.session != 0, "zero session handle");
400        Ok(())
401    }
402    async fn exchange(&mut self, s: &mut TcpStream, a: &Value) -> Result<Value> {
403        validate(a)?;
404        if a["type"] == "ethernet_ip_discover" {
405            let f = self.send(s, 0x63, &[]).await?;
406            let p = &f[24..];
407            ensure!(
408                p.len() >= 39
409                    && u16le(p) == 1
410                    && u16le(&p[2..]) == 0x0c
411                    && u16le(&p[4..]) as usize + 6 == p.len(),
412                "invalid identity list"
413            );
414            let v = &p[6..];
415            let len = v[32] as usize;
416            ensure!(v.len() == 34 + len, "identity name length");
417            return Ok(
418                json!({"vendor":u16le(&v[18..]),"device_type":u16le(&v[20..]),"product":u16le(&v[22..]),"serial":u32le(&v[28..]),"name":std::str::from_utf8(&v[33..33+len])?}),
419            );
420        }
421        let service = if a["type"] == "ethernet_ip_get" {
422            0x0e
423        } else {
424            0x10
425        };
426        let mut path = vec![];
427        for (key, segment) in [("class", 0x20), ("instance", 0x24), ("attribute", 0x30)] {
428            let n = number(a, key, 65535)? as u16;
429            if n <= 255 {
430                path.extend([segment, n as u8]);
431            } else {
432                path.extend([segment + 1, 0]);
433                path.extend(n.to_le_bytes());
434            }
435        }
436        let mut cip = vec![service, (path.len() / 2) as u8];
437        cip.extend(path);
438        if service == 0x10 {
439            cip.extend(encode_value(a)?);
440        }
441        let f = self.send(s, 0x6f, &cpf(&cip)).await?;
442        let p = cip_payload(&f[24..])?;
443        ensure!(
444            p.len() >= 4 && p[0] == service | 0x80 && p[1] == 0 && p.len() >= 4 + 2 * p[3] as usize,
445            "CIP response header"
446        );
447        if p[2] != 0 {
448            return Ok(json!({"success":false,"status":p[2]}));
449        }
450        let body = &p[4 + 2 * p[3] as usize..];
451        if service == 0x10 {
452            ensure!(body.is_empty(), "unexpected write response data");
453            Ok(json!({"success":true}))
454        } else {
455            Ok(
456                json!({"success":true,"value":decode_value(a["value_type"].as_str().context("type")?,body)?}),
457            )
458        }
459    }
460}