I don't recognise that description of TFTP, I don't know of a 'session ID'.
TFTP is the Trivial File Transfer Protocol as described by Public RFC 1350. I'm working specifically with Revision 2. 'Session ID' refers to Transfer ID, or TID. I'm not sure how you could recognize TFTP and not recognize a reference to Session ID, but hopefully this clarification will help.
I don't think you need any of the complexity of raw sockets and manually built packets at all. All you need and 1+N UdpClient instances. One on port 69, and one each for each client that connects. From TCP/IP Illustrated Volume 1 "The server [program] then allocates some other unused ephemeral port on the server's host, which is then used by the server for all further packet exchange[s]."
TCP specifications are not relevant here, as TFTP relies explicitly and strictly on UDP which relies explicitly and strictly in IPvX (X could be 4 or 6).
A TFTP TID is composed of the Server Port and Client Port as reported directly by the UDP Header.
On the Client Side, UDP SourcePort = Server Port and UDP Destination Port = Client Port.
On the Server Side, UDP SourcePort = Client Port and UDP Destination Port = Server Port.
The Source and Destination IP Addresses as reported by the IP Header are not referenced in the TFTP Specification itself, however real-world testing has proven conclusively that it is extraordinarily meaningful vis a vis hardware and software firewalls (and also that some TFTP Implementations expand the TID to include IP Addresses as well as Ports).
So for example:
Using a System.Net.Sockets.UDPClient, I can receive a WRQ or RRQ Packet on Port 69, and I can retrieve the RemoteIPEndPoint and begin a Session that way, however if I bind the Client to System.Net.IPAddress.Any on the Local side there is NO way at all for me to determine WHAT local IP Address it was received on. In any system with multiple network Adapters (or even multiple IP Addresses) this makes the IP and UDP Header data critical to any meaningful response.
I'm in exactly the same boat with System.Net.Sockets.Socket when it's instantiated with the Dgram Type and UDP Protocol.
So using the RAW Type and the UDP Protocol, I can extract the meaningful information from the IP and UDP Headers. What I can't do at that point is Bind the listening socket to anything but an IP Address, because no matter what Socket is specified it will return ALL UDP Datagrams that come in on ANY Port. I'd pretty obviously rather not process and route all UDP traffic that comes in.
At the same time, if I have the first Socket listening for all the data that's coming in on the appropriate TID Port, I need to be able to use a second Socket to send data out to the remote host or client.
With all of this in mind:
While the first Socket is bound to a local network address and port the second Socket cannot be bound to the same address and port because DotNET throws an exception, and binding the socket is the ONLY way to get the automatically generated IP and UDP Headers set up to send the target system the meaningful TID data. Using my custom IPv4 and UDP Datagrams I can manage to bind the second Socket to the appropriate local IP on any available Port and then send the by-hand UDP Datagram with the meaningful Source/Destination Ports, but I'd rather have the ability to push data onto the network using any available local adapter using my own constructed-by-hand full IPv4+UDP+TFTP Packet structures.
It never hurts to try. In a worst case scenario, you'll learn from it.