So that happens for both packets sent by a PPP client and packets received from other clients(also on PPP) or from the gateway or other non-PPP IPv4 clients on the network?
Right now, my implementation (which has multiple PPP clients connected to a real network with other clients and a router (the default gateway, which is it's default gateway if it can't match the host network and not matched to the clients' own network ID)).
Most of the handling is performed by the ARP (or virtual ARP if it's on the server's internal network (which has all connected PPP clients on it)).
The ARP routine for sending a packet checks:
- Destination on the clients' network numer (same subnet)? Then send to the internal loopback (to be received by all clients, defaulting to a special reserved MAC address for handling such a case, the clients each have MAC addresses following that to keep them seperate).
-- If the above matches and it's a broadcast within the subnet, change the destination MAC address to broadcast instead.
Otherwise:
- Host network matched for the PC that the server is running on? Then
-- Network address matched (for example for a /24 host network this is ending in .0)? Then invalidate the 'ARP' request and broadcast instead.
-- Otherwise, perform an actual ARP for the destination IP address. It can replace the IP address of the requester and it's MAC address with the configured default gateway (to simulate it coming from the router or otherwise the host if that's not configured)
-- The answer for the ARP is waited for 500ms. If obtained within, give the result. Otherwise, retry. It has a 30 second timeout on the ARP timer, to prevent the (new) cached value from going stale (so after 30 seconds the single-entry cache is effectively cleared).
-- If the ARP request times out (after 500ms), it fails the request and sends it to the host's MAC address instead.
- Finally, if the host network isn't matched, it simple tries to send it to the default gateway.
All sending of packets is through a global packet sending routine:
- If IP and not broadcasting, perform ARP (using the above algorithm). If it has an ARP address, replace it in the packet to send. If loopback is returned, perform a loopback into the packet receiver directly (not sending on the host network).
-- Otherwise, if IP packet to non-broadcast IP, If sending to the clients or hosts broadcast address (including subnets):
-- Change MAC to broadcast.
--- If sending to the client's broadcast address (or it's subnet's broadcast), perform a loopback. Otherwise, discard.
- If none of the above apply, or loopback, loopback the packet into a buffer (used by the pcap receiver). Otherwise, if possible, send the packet onto the host computer's network using pcap.
The pcap receiver (in another CPU thread) receives packets from either the loopback buffer or the host's pcap interface. The loopback buffer has priority on receiving packets.
If it receives a packet, it registers it internally for use and when from the pcap interface, it discards packets to it's loopback adapter's MAC address (it's an indicator loopback is used, so to prevent conflicts the packets from the host machine are ignored when from that MAC address). Otherwise, continue to the next step.
The next step is that the EtherType field is examined. If it's incompatible with what the virtual modem supports, it's ignored and discarded. Otherwise, it starts into the first phase of the receiver handling.
The first phase of the receiver handling checks for any eligible clients that can receive the packet. If all clients that are eligible are clear to receive the packet, the receiver continues to proceed to move the buffer over to the global receiver buffer for all clients that are connected.
This step also filters eligible clients by:
- If not the clients' IP or broadcast address zero (0.0.0.0) then if it isn't a broadcast address, it's not eligible for the client.
- Otherwise, if it's the host machine's subnet-based broadcast address and not from the loopback, it's ignored for the client.
Next, there's also a little check that checks not the local (reserved) MAC and broadcast MAC. If that happens and it's an IPv4 packet with the local reserved MAC (thus loopback special cased), it's ignored for the client.
Next, if all above succeed, ARP replies are checked and registered for the ARP handler if it receives a reply for it's current request.
Next it checks for DHCP pending (for future implementation) and aborts further processing if so. Right now this shouldn't happen yet.
Up next is that it checks the MAC address of the destination for not being the client's own MAC or broadcast MAC. If that's the case, ignore if the packets is addressed to the local reserved MAC and it's an IP packet (in other words, a loopback to ourselves).
Next, it processed ARP requests and sends back a reply using the same loopback or the real ethernet, depending on it's source.
Then it checks if the packet server client is in it's active state (communicating with the host using PPP IP/IPX/other protocol packets. If not, ignore the packet for this client.
Otherwise, check if the packet buffer of the client is empty to receive it (3 buffers, one for IPv4, one for EFD5 and one for all other packet types (in this case IPX for current builds)).
If it's indeed empty, proceed to the final phase of receiving: move the packet to the buffer and for IPX packets increase hop count and discard if exceeded. The loopback buffer is emptied at this point as well (as it's properly parsed).
The main routine performs much the same, but for each client in parallel. It empties the filled global 3 packet buffers into the all client's 3 packet buffer, then lets the client parse it in it's own time (according to it's transmission timing with the connected cleint etc.).
The main difference here is that it has no ARP handling, handles PPPOE (if used on the host network for passthrough PPPOE connections), parses the packets once again using the same checks (to make sure each client that matches receives the packet in parallel with all other clients, making receiving multicast possible) and handles the connected clients' PPP connections (or SLIP-based connection using ethernet packets (it has an official name, which I can't remember atm) and all the stuff for the other protocols (like EDFS, IPX etc.) as well as PPP packet encryption.
It also performs the actual routing to the host network for physical IPX/IPv4/EDFS packets using the Ethernet II protocol encapsulation, a text (ASCII) terminal on the client's connection for logging in, half implemented DHCP support (disabled).
Then in the outside connection handling (outside the client loop), there's a loop handling timed data I/O to all TCP connections (which are virtual modems like Dosbox, UniPCemu etc. dialing into the server).
One nice little thing it also supports is that the whole modem interface can be turned inside-out, with instead of the serial modem's interface being connected to a serial port inside the x86 emulator, it's instead connected to a real (or virtual if drivers are installed on the host) serial port, allowing other devices (or funnily enough, even the host itself if using a loopback serial port of sorts, like I tested and use with com0com for example, providing the same dial-up network to the windows machine for connecting to it's clients the same way (and sharing a seperate network, that's inside the real network that Windows is on as well (unless the server is configured to be in loopback mode) 🤣 )).
That's basically how it works right now.
The host and client broadcast addresses are detected in a simple way, using the known netmasks and addresses:
There's a function that checks if the address is a broadcast address for the client (priority) or the host.
There's a function that checks if an IP is the client's subnet IP (destination and client IP addresses match when masked using the client's subnet mask).
There's a function for getting the client's broadcast address (when subnet mask isn't all-ones, it's giving the client's IP, subnet masked and the host bits all set).
There's a function for validating incoming multicast addresses (first 3 bits of the top 4 bits set). Right now all multicast addresses are considered valid (unknown how to handle them right now).
Much like there's a function to get the client's broadcast address, there's one for the host machine as well.
Then the main check is the client broadcast address validation function. It matches if:
- destination IP masked matches client IP masked and client IP is non-zero, then if masked with host bits set, it's the broadcast address for the client's network (broadcast type 1). Otherwise, check more below.
- If multicast, then multicast (type 3).
- If zero address (0.0.0.0), count the same as 255.255.255.255 below.
- If 255.255.255.255 broadcast, report broadcast type 2.
- Otherwise, give no broadcast (type 0).
The host function performs much the same, but using the host address and subnet mask instead.
That's pretty much roughly how it's implemented right now.