Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Received length loss #168

Closed
wfeii1980 opened this issue Feb 1, 2024 · 21 comments
Closed

Received length loss #168

wfeii1980 opened this issue Feb 1, 2024 · 21 comments

Comments

@wfeii1980
Copy link

wfeii1980 commented Feb 1, 2024

This question comes from last branch main.

    fn handle_read(&mut self, ip: &IpAddr) -> bool {
        let intf_sock = match self.intf_socks.get_mut(ip) {
            Some(if_sock) => if_sock,
            None => return false,
        };
        let mut buf = vec![0u8; MAX_MSG_ABSOLUTE];
        let sz = match intf_sock.sock.read(&mut buf) {
            Ok(sz) => sz,
            Err(e) => {
                if e.kind() != std::io::ErrorKind::WouldBlock {
                    error!("listening socket read failed: {}", e);
                }
                return false;
            }
        };

        debug!("received {} bytes", sz);

        // If sz is 0, it means sock reached End-of-File.
        if sz == 0 {
            error!("socket {:?} was likely shutdown", intf_sock);
            if let Err(e) = self.poller.delete(&intf_sock.sock) {
                error!("failed to remove sock {:?} from poller: {}", intf_sock, &e);
            }

            // Replace the closed socket with a new one.
            match new_socket_bind(&intf_sock.intf) {
                Ok(sock) => {
                    let intf = intf_sock.intf.clone();
                    self.intf_socks.insert(*ip, IntfSock { intf, sock });
                    debug!("reset socket for IP {}", ip);
                }
                Err(e) => error!("re-bind a socket to {}: {}", ip, e),
            }
            return false;
        }

        // ******************** buf.len() == MAX_MSG_ABSOLUTE != sz
        match DnsIncoming::new(buf) {
            Ok(msg) => {
                if msg.is_query() {
                    self.handle_query(msg, ip);
                } else if msg.is_response() {
                    self.handle_response(msg);
                } else {
                    error!("Invalid message: not query and not response");
                }
            }
            Err(e) => error!("Invalid incoming DNS message: {}", e),
        }

        true
    }
@wfeii1980
Copy link
Author

buf.set_len(sz) is unsafe
buf.truncate(sz) is ok

@wfeii1980
Copy link
Author

Two panic!

        for _ in 0..n {
            let name = self.read_name()?;
            let slice = &self.data[self.offset..];
            // ***************** slice.len() < 10
            let ty = u16_from_be_slice(&slice[..2]);
            let class = u16_from_be_slice(&slice[2..4]);
            let ttl = u32_from_be_slice(&slice[4..8]);
            let length = u16_from_be_slice(&slice[8..10]) as usize;
            self.offset += 10;
            let next_offset = self.offset + length;
            // Check the first 2 bits for possible "Message compression".
            match length & 0xC0 {
                0x00 => {
                    // regular utf8 string with length
                    offset += 1;
                    // **************** (offset + length as usize) > data.len()
                    name += str::from_utf8(&data[offset..(offset + length as usize)])
                        .map_err(|e| Error::Msg(format!("read_name: from_utf8: {}", e)))?;
                    name += ".";
                    offset += length as usize;
                }

@wfeii1980
Copy link
Author

In my environment, MDNS has a relatively large amount of data, and the problem of data loss is often caused by the read cache being full. So it also causes the above problems.

@keepsimple1
Copy link
Owner

keepsimple1 commented Feb 1, 2024

buf.set_len(sz) is unsafe buf.truncate(sz) is ok

I couldn't remember why we didn't truncate the buffer after the read. One possible reason is DnsIncoming decoding the buffer does not require the truncate (The decoding relies on the message format). But anyhow we can add buf.truncate(sz).

@keepsimple1
Copy link
Owner

// ***************** slice.len() < 10

I think it's a good idea to add a check for the slice length and return Err if fails.

@keepsimple1
Copy link
Owner

// **************** (offset + length as usize) > data.len()

Same here, I agree it's good to check the length here.

@keepsimple1
Copy link
Owner

keepsimple1 commented Feb 1, 2024

In my environment, MDNS has a relatively large amount of data, and the problem of data loss is often caused by the read cache being full. So it also causes the above problems.

What the typical and max size of your mDNS message? Did you mean the mDNS message in your env. is larger than MAX_MSG_ABSOLUTE i.e. 8966 bytes? The RFC says a Multicast DNS packet, including IP and UDP headers, MUST NOT exceed 9000 bytes (here).

@keepsimple1
Copy link
Owner

For the sanity checks, I've opened a PR to add them. Let me know if you have any questions or comments.

@keepsimple1
Copy link
Owner

PR merged. Let me know if you have any questions.

@wfeii1980
Copy link
Author

In my environment, MDNS has a relatively large amount of data, and the problem of data loss is often caused by the read cache being full. So it also causes the above problems.

What the typical and max size of your mDNS message? Did you mean the mDNS message in your env. is larger than MAX_MSG_ABSOLUTE i.e. 8966 bytes? The RFC says a Multicast DNS packet, including IP and UDP headers, MUST NOT exceed 9000 bytes (here).

I did receive an oversized package, if it's illegal, then ignore it.

mdns-big.zip

@wfeii1980
Copy link
Author

PR merged. Let me know if you have any questions.

Good!

@keepsimple1
Copy link
Owner

keepsimple1 commented Feb 4, 2024

In my environment, MDNS has a relatively large amount of data, and the problem of data loss is often caused by the read cache being full. So it also causes the above problems.

What the typical and max size of your mDNS message? Did you mean the mDNS message in your env. is larger than MAX_MSG_ABSOLUTE i.e. 8966 bytes? The RFC says a Multicast DNS packet, including IP and UDP headers, MUST NOT exceed 9000 bytes (here).

I did receive an oversized package, if it's illegal, then ignore it.

mdns-big.zip

According to WireShark, the packet size is 1030 bytes, far from the max size (8966 bytes). I didn't see obvious problems here. Let me know if you encountered any particular issues.

mdns-sd-wireshark-2024-02-04

@wfeii1980
Copy link
Author

All 11 frames form one package.

wireshark-mdns-big

@keepsimple1
Copy link
Owner

Oh I missed that, sorry. It is a fragmented IP packet. We don't support fragmented IP packets yet. This would be a new feature and it will take some time to implement if I would do it. Would you be interested in creating a PR to add this new feature to support IP fragments?

@dalepsmith
Copy link
Contributor

dalepsmith commented Feb 5, 2024 via email

@keepsimple1
Copy link
Owner

keepsimple1 commented Feb 5, 2024

Regarding IP fragmentation, it seems that UDP packet received by the socket should already be reassembled, if no dropped packets. From ChatGPT:
In summary, IP handles fragmentation and reassembly, and this applies to both TCP and UDP traffic. UDP itself, being a simple and connectionless protocol, doesn't have built-in mechanisms for handling fragmentation or reassembly.
That means we need not (and should not) have special logic to handle IP fragments.

And as what Dale mentioned :

Basically, an mdns datagram can not be larger than 9000 even when
fragmented into multiple UDP packets. And also can not contain more
than one resource record.

We should be good with the current max size (around 9000). For any large RR (resource record), it should be sent in a separate mDNS datagram.

Looking back at your capture, it seems that all 377 RRs are sent in a single datagram of 15794 bytes, which conflicts the RFC recommendation.

mdns-wireshark-large

@dalepsmith
Copy link
Contributor

dalepsmith commented Feb 5, 2024 via email

@wfeii1980
Copy link
Author

I'm curious what is generating that huge datagram! Is that Bonjour or Avahi? Some other library? Something homebrewed? What kind of device is sending that? Thanks! -Dale

I guess it comes from the jmdns package used by my colleague, but I haven't personally verified it yet.

@keepsimple1
Copy link
Owner

I don't think we need to support such datagram that is clearly outside of the RFC. On the other hand, I will think of how we can handle such cases more gracefully.

@keepsimple1
Copy link
Owner

keepsimple1 commented Feb 6, 2024

I had to re-learn some details about UDP socket API. Here is a related info about the recv/recv_from API on Linux:

If a message is too long to fit in the supplied buffer, excess bytes may be discarded depending on the type of socket the message is received from.

and in the classic book "TCP/IP Illustrated, volume 1", chapter 11, section 11.10 Maximum UDP Datagram Size, it says this about "Datagram Truncation":

What happens if the received datagram exceeds the size the application is prepared to deal with? Unfortunately the answer depends on the programming interface and the implementation.

It then says the sockets API under SVR4 Unix does not truncate the datagram.

Based on the above, I think there is no much more we can do (or should do) besides the sanity checks in the current code.

If a mDNS datagram happens to exceed the max buffer size, it will not be decoded correctly / fully. And if there is no truncation, the follow-up read will have invalid headers, etc, and will fail the decode process with an Err return. As long as there is no crash, I think we are good.

@keepsimple1
Copy link
Owner

I've merged in the minor change of MAX_MSG_ABSOLUTE and added comments. Will close this issue. If you have any additional questions, please re-open this issue or open a new issue. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants