Chapter 6 - The User Datagram Protocol


SummaryThe User Datagram Protocol provides a low-overhead transport service for application ptotocols that do not need (or cannot use) the connection-oriented services offered by TCP. UDP is most often used with applications that make heavy use of broadcasts or multicasts, as well as applications that need fast turnaround times on lookups and queries.
Protocol ID17
Relevant STDs2 (http://www.iana.org/);
3 (includes RFCs 1122 and 1123);
6 (RFC 768, republished)
Relevant RFCs768 (User Datagram Protocol);
1122 (Host Network Requirements)

There are two standard transport protocols that applications use to communicate with each other on an IP network. These are the User Datagram Protocol (UDP), which provides a lightweight and unreliable transport service, and the Transmission Control Protocol (TCP), which provides a reliable and controlled transport service.

The majority of Internet applications use TCP, since its built-in reliability and flow control services ensure that data does not get lost or corrupted. However, many applications that do not require the overhead found in TCP—or that cannot use TCP because the application has to use broadcasts or multicasts—will use UDP instead. UDP is more appropriate for any application that has to issue frequent update messages or that does not require every message to get delivered.

The UDP Standard

UDP is defined in RFC 768, which has been republished as STD 6 (UDP is an Internet Standard protocol). However, RFC 768 contained some vagaries that were clarified in RFC 1122 (Host Network Requirements). As such, UDP implementations need to incorporate both RFC 768 and RFC 1122 in order to work reliably and consistently with other implementations.

RFC 768 states that UDP is a stateless, unreliable transport protocol that does not guarantee delivery. Thus, UDP is meant to provide a low-overhead transport for applications to use when they do not need guaranteed delivery.

RFC 768 also states that the Protocol ID for UDP is 17. When a system receives an IP datagram that is marked as containing Protocol 17, it should pass the contents of the datagram to the local UDP service for further processing.

UDP Is an Unreliable, Datagram-Centric Transport Protocol

As we discussed in Chapter 1, An Introduction to TCP/IP, sending a message via UDP is somewhat analogous to sending a postcard in that it is totally untrustworthy, providing no guarantees of any kind of delivery. UDP messages are sent and then forgotten about immediately. As such, applications that need a reliable transport protocol should not use UDP.

However, UDP's lightweight model does provide some distinct benefits, particularly in comparison to TCP's highly managed connection model. While TCP provides high levels of reliability through highly managed virtual circuits, UDP offers high performance from having so little overhead. If reliability comes at the expense of performance, then conversely, performance can be gained by eliminating some of the overhead associated with reliability.

In addition, many applications simply cannot use TCP, since TCP's virtual circuit design requires dedicated end-to-end connections between two (and no more than two) endpoints. If an application needs to use broadcasts or multicasts in order to send data to multiple hosts simultaneously, then that application will have to use UDP to do so.

Limited reliability

Although applications that broadcast information on a frequent basis have to use UDP, they do gain some benefits from doing so. Since broadcasts are sent to every device on the local network, it would take far too long for the sender to establish individual TCP connections with every other system on the network, exchange data with them all, and then disconnect. Conversely, UDP's connectionless service allows the sender to simply send the data to all of the devices simultaneously. If any of the systems do not receive one of the messages, then they will likely receive one of the next broadcasts, and as such will not be substantially harmed by missing one or two of them.

Furthermore, streaming applications (such as real-time audio and video) can also benefit from UDP's low-overhead structure. Since these applications are streamoriented, the individual messages are not nearly as important as the overall stream of data. The user will not notice if a single IP packet gets lost every so often, so it is better to just continually keep sending the next message, rather than stopping everything to resend a single message. These applications actually see errorcorrection as a liability, so UDP's connectionless approach is a feature rather than a problem.

Similarly, any application that needs only a lightweight query and response service would be unduly burdened by TCP's connection-oriented services and would benefit from UDP's low overhead. Some database and network-lookup services use UDP for just this reason, allowing a client and server to exchange data without having to spend a lot of time establishing a reliable connection when a single query is all that's required.

It should be pointed out that many of the applications that use UDP require some form of error correction, but that this error correction also tends to be specific to the application at hand, and is therefore embedded directly into the application logic. For example, a database client would need to be able to tell when no response came back from a query, and so the database client may choose to just reissue the entire query rather than try to fix a specific part of the datastream (this is how Domain Name System queries work). Applications that use UDP must therefore incorporate any required error-checking and fault-management routines internally, rather than rely on UDP to provide these services.

Another interesting point is that most of the network technologies in use today are fairly reliable to begin with, so unreliable protocols like UDP (and IP) are likely to reach their destinations without much problems. Most LANs and WANs are extremely reliable, losing only tiny amounts of data over the course of their lifetime. On these types of networks, UDP can be used without much concern. Even topologies that are unreliable (such as analog modems) typically provide a modicum of error-correction and retransmission services at the data-link layer.

For these reasons, UDP probably shouldn't be considered totally unreliable, although you must always remember that UDP doesn't provide any error-correction or retransmission services within the transport itself. It just inherits any existing reliability that is provided by the underlying medium.

Furthermore, UDP also provides a checksum service that allows an application to verify that whatever data has arrived is probably the same as that which was sent. The use of UDP's checksum service is optional, and not all of the applications that use UDP also use the checksum service (although they are encouraged to do so by RFC 1122). Some applications incorporate their own verification routines within the UDP data segment, augmenting or bypassing UDP's provisional data-verification services with application-specific equivalents.

Datagram-centric transport services

Another unique aspect of UDP is the way in which it deals with only one datagram at a time. Rather than attempting to manage a stream of application data the way that TCP does, UDP deals with only individual blocks of data, as generated by the application protocols in use. For example, if an application gives UDP a fourkilobyte block of data, then UDP will hand that data to IP as a single datagram, without trying to create efficient segment sizes (one of TCP's most significant traits). The data may be fragmented by IP when it builds and sends IP packets for that four-kilobyte block of data, but UDP does not care if this happens and is not involved in that process whatsoever.

Furthermore, since each IP datagram contains a fully formed UDP datagram, the destination system will not receive any portion of the UDP message until the entire IP datagram has been received. For example, if the underlying IP datagram has been fragmented, then UDP will not receive any portion of the message until all of the fragments have arrived and been reassembled by IP. But once that happens, then UDP (and the application in use with UDP) will get and read the entire fourkilobyte message in one shot.

owl.gif Some UDP stacks require that the application have enough buffers to read the entire datagram. If the application cannot accept all of the data, then it will not get any of the data, since the datagram will be discarded.

Conversely, remember that TCP does not generally cause fragmentation to occur, since it attempts to avoid fragmentation through the use of efficiently sized segments. In that model, TCP would send multiple TCP segments, each of which could arrive independently and be made available to the destination application immediately. Although there are benefits to the TCP design, record-centric applications also have to perform more work when using it instead of UDP, since UDP provides one-shot access to all of the data.

In fact, UDP is particularly useful for applications that have to transfer fixed-length records of data (such as database records or even fixed-length files). For example, if an application needs to send six records from a database to another system, then it can generate six fixed-length UDP datagrams, and UDP will send those datagrams as independent UDP messages (which become independent IP datagrams). The recipient then receives the datagrams as self-contained records and will be able to immediately process them as six unique records.

In contrast, TCP's circuit-centric model would require the same application to write the data to the TCP virtual circuit, which would then break the data into segments for transport across the network. The recipient would then have to read through the segments as they arrived at the destination system, poking through the data and looking for end-of-record markers until all six records were received and found.

For all of these reasons, UDP is a more efficient protocol, although it is still unreliable. As such, application protocols that want to leverage the low-overhead nature of UDP must provide their own reliability services. In addition, these applications typically have to provide their own flow-control and packet-ordering services, ensuring that datagrams are not received out of order. Most applications incorporate a half-duplex data-exchange mechanism in order to provide these services. The application protocol waits for a clear-to-send signal from the remote system, transmits a datagram, and then stops to wait for the clear-to-send signal again.

For example, Trivial File Transfer Protocol (TFTP) clients use acknowledgment messages embedded in UDP datagrams to tell a server that it received the last block of data and that it is ready to receive another block. The TFIP server then sends another block of data as another UDP message and then wait to receive an acknowledgment before sending another block. Although this method is clumsy when compared to TCP's graceful sliding window concept, it has been proven to work over the years.

UDP Ports.

UDP does very little. In fact, it does almost nothing, acting only as a very basic facilitator for applications to use when they need to send or receive datagrams on an IP network. In order to perform this task, UDP has to provide two basic services: it must provide a way for applications to send data over the IP software, and it must also provide a way to get data that it has received from IP back to the applications that need it.

These services are provided by a multiplexing component within the UDP software. Applications must register with UDP, allowing it to map incoming and outgoing messages to the appropriate application protocols themselves.

This multiplexing service is provided by 16-bit port numbers that are assigned to specific applications by UDP. When an application wishes to communicate with the network, it must request a port number from UDP (server applications such as TFTP will typically request a pre-defined, specific port number, while most client applications will use whatever port number they are given by UDP). UDP will then use these port numbers for all incoming and outgoing datagrams

This concept is illustrated in Figure 6-1. Each of the applications that are using UDP have allocated a dedicated port number from UDP, which they use for all incoming and outgoing data.

0255-01.gif
Figure 6-1.
Application-level multiplexing with port numbers

When an application wishes to send data over the network, it gives the data to UDP through the assigned port number, also telling UDP which port on the destination system the data should be sent to. UDP then creates a UDP message, marking the source and destination port numbers, which is then passed off to IP for delivery (IP will create the necessary IP datagram).

Once the IP datagram is received by the destination system, the IP software sees that the data portion of the IP datagram contains a UDP message (as specified in the Protocol Identifier field in the IP header), and hands it off to UDP for processing. The UDP software looks at the UDP header, sees the destination port number, and hands the payload portion of the datagram to whatever application is using the specified port number. Figure 6-2 illustrates this concept using the Trivial File Transfer Protocol (TFTP), a small file transfer protocol that uses UDP.

0256-01.gif
Figure 6-2.
Data being sent from a TFTP client to a TFTP server

Technically, a port identifies only a single instance of an application on a single system. The term socket is used to identify the port number and IP address concantenated together (i.e., port 80 on host 192.168.10.10 would be referred to as the socket 192.168.10.10:80). Finally, a socket pair consists of both endpoints, including the IP addresses and port numbers of both applications on both systems. Multiple connections between two systems must have unique socket pairs, with at least one of the two endpoints having a different port number.

Although the concept of socket pairs with UDP is similar to the same concept as it works with TCP, there are some fundamental differences that must be taken into consideration when looking at how connections work with UDP versus how they work with TCP. Most importantly, while TCP can maintain multiple virtual circuits on a single port number through the use of socket pairs, UDP-based applications do not have this capability at all, and simply treat all data sent and received over a port number as data for a single connection.

For example, if a DNS server is listening for queries on port 53, then any queries that come in to that port are treated as equal, with the DNS server handling the multiplexing services required to distinguish between the different clients that are issuing the distinct queries. This is the opposite of how TCP works, where the transport protocol would create and manage virtual circuits for each of the connections. With UDP, all data is treated as a single connection, and the application must manage any multiplexing services required on that port.

Well-known ports

Most server-based IP applications use what are referred to as well-known port numbers. For example, a TFTP server will listen on UDP port 69 by default, which is the well-known port number for TFTP servers. This way, any TFTP client that needs to connect to any TFTP server can use the default destination of UDP port

69. Otherwise, the client would have to specify the port number of the server that it wanted to connect with (you've seen this in some URLs that use http://www. somehost.com:8080/ or the like; 8080 is the port number of the HTTP server on www.somehost.com).

Most application servers allow you to use any port number you want. However, if you run your servers on non-standard ports, then you would have to tell every user that the server was not accessible on the default port. This would be a hard-to-manage implementation at best. By sticking with the defaults, all users can connect to your server using the default port number, which is likely to cause the least amount of trouble.

owl.gif Some network administrators purposefully run application servers on nonstandard ports, hoping to add an extra layer of security to their network. However, it is my opinion that security through obscurity is no security at all and that this method should not be relied upon by itself.

Historically, only servers have been allowed to run on ports below 1024, as these ports could be used only by privileged accounts. By limiting access to these port numbers, it was more difficult for hacker to install a rogue application server. However, this restriction is based on Unix-specific architectures, and is not easily enforced on all of the systems that run IP today. Many application servers now run on operating systems that have little or no concept of privileged users, making this historical restriction somewhat irrelevant.

There are a number of predefined port numbers that are registered with the Internet Assigned Numbers Authority (IANA). All of the port numbers below 1024 are reserved for use with well-known applications, although there are also many applications that use port numbers outside of this range. Some of the more common port numbers are shown in Table 6-1. For a detailed listing of all of the port numbers that are currently registered, refer to the IANA's online registry (accessible at http://www.isi.edu/in-notes/iana/assignments/port-numbers).

Table 6-1. Some of the Port Numbers Reversed for Well-Known UDP Servers
Port NumberDescription
53Domain Name System (DNS)
69Trivial File Transfer Protocol (TFTP)
137NetBIOS Name Service (sometimes referred to as WINS)
161Simple Network Management Protocol (SNMP)

Besides the reserved addresses that are managed by the IANA, there are also unreserved port numbers that can be used by any application for any purpose, although conflicts may occur with other users who are also using those port numbers. Any port number that is frequently used is encouraged to register with the IANA.

To see the well-known ports used on your system, examine the /etc/services file on a Unix host, or the C:\WinNT\System32\Drivers\Etc\SERVICES file on a Windows NT host.

The UDP Header

UDP messages consist of header and body parts, just like IP datagrams. The body part contains whatever data was provided by the application in use, while the header contains the fields that tell the destination UDP software what to do with the data.

A UDP message is made up of six fields (counting the data portion of the message). The total size of the message will vary according to the size of the data in the body part. The fields in a UDP message are shown in Table 6-2, along with their size (in bytes) and their usage.

Table 6-2. The fields in a UDP Message
FieldBytesUsage Notes
Source Port2Identifies the 16-bit port number in use by the application that is sending the data
Destination Port2Identifies the 16-bit target port number of the application that is to receive this data
Length2Specifies the size of the total UDP message, including both the header and data segments
Checksum2Used to store a checksum of the entire UDP message
DatavariesThe data portion of the UDP message

Notice that the UDP header does not provide any fields for source or destination IP addresses, or for any other services that are not specifically related to UDP. This is because those services are provided by the IP header or by the application-specific protocols (and thus contained within the UDP message's data segment).

Every UDP message has an eight-byte header, as can be seen from Table 6-2. Thus, the theoretical minimum size of a UDP message is eight bytes, although this would not leave any room for any data in the message. In reality, no UDP message should ever be generated that does not contain at least some data.

Figure 6-3 shows a UDP message sent from a TFTP client to a TFTP server. In that example, a TFTP session is opened between Greywolf (the client) and Arachnid (the server), with Greywolf sending a file (called testfile.txt) to Arachnid. We'll use this message for further discussion of the UDP header fields.

0259-01.gif
Figure 6-3.
A simple UDP message

The following sections describe the header fields of the UDP message in detail.

Source Port

Identifies the message's original sender, as referenced by the 16-bit UDP port number in use by the application.

Size
Sixteen bits.

Notes
This field identifies the port number used by the application that created the data.

Note that RFC 768 states Source Port is an optional field, when meaningful, it indicates the port of the sending process, and may be assumed to be the port to which a reply should be addressed in the absence of any other information. If not used, a value of zero is inserted.
Although Source Port is optional, it should always be used.

Capture Sample
In the capture shown in Figure 6-4, the Source Port field is set to hexadecimal 04 2c, which equates to decimal 1068.

0260-01.gif
Figure 6-4.
The Source Port field

See Also
Destination Port

UDP Ports

Destination Port

Identifies the message's destination, as referenced by the 16-bit UDP port number in use by the application on the destination system.

Size
Sixteen bits.

Notes
This field identifies the port number used by the

destination application.

Capture Sample
In the capture shown in Figure 6-5, the Destination Port field contains the hexadecimal value of 00 45, which equals decimal 69, the well-known port number for TFTP servers.

0261-01.gif
Figure 6-5.
The Destination Port field

See Also
Source Port

UDP Ports

Length

Specifies the length of the entire UDP message in bytes, including both the header and data segments.

Size
Sixteen bits.

Notes
The primary purpose of this field is to inform a system of where the message ends. All UDP headers are eight bytes long, so the minimum size of a UDP message is eight bytes, while the maximum size is 65,535 bytes minus the size of the IP header (which is normally 20 bytes). Determining the size of the data portion of the UDP message can be found by subtracting eight (the size of the UDP header) from the value in this field.

The size of the UDP message is determined by the application. Whatever data gets generated by the application gets sent to IP as one big chunk of data by UDP, which gets turned into one big IP datagram. If the IP datagram is too big
for IP to deliver (due to a restricted MTU size), then it is up to IP to fragment the datagram into appropriately sized IP packets, and to get those fragments to the destination system.
If the IP packets require fragmentation but the Don't Fragment bit is set, then the packets will be discarded, and an ICMP Fragmentation Required But DF Set Error Message should be returned to UDP on the sending system. If the UDP layer receives this message, it should relay it back to the calling application, where any necessary error-recovery can be conducted. However, if the application has already gone away, then the error message will never get seen (and therefore never get dealt with). Furthermore, many UDP applications do not care if there are problems after-the-fact, and do not monitor for ICMP error messages at all.

Capture Sample
In the capture shown in Figure 6-6, the Length field shows that this UDP message is 32 bytes long, indicating that the data segment is 24 bytes long (the UDP header is always eight bytes long).

0262-01.gif
Figure 6-6.
The Length field

See Also
Fragmentation Flags in Chapter 2, The Internet Protocol

Destination Unreachable error messages in Chapter 5, The Internet Control Message Protocol

Checksum

Used to store a checksum of the UDP message, including both the header and body parts. The checksum allows the destination system to validate the contents of the UDP message and to test for possible data corruption.

Size
Sixteen bits.

Notes
The UDP checksum is entirely optional. Many applications do not use it, particularly those that are performance-sensitive, where the additional processing time required for checksum calculations would induce undesirable delays. However, checksums do provide a certain amount of reliability, and they should be used whenever possible.

Even RFC 1122 states that UDP checksums should be enabled by default (requiring that the user disable them manually). Furthermore, RFC 1122 also states that if a recipient system receives a UDP message with a checksum, then it must use that checksum to verify the integrity of the data. Thus, sending a message without a checksum is optional, but reading and verifying any checksum that does exist is mandatory.
If the checksum is deemed to be invalid, then the message is discarded. Discarding segments is a silent error, and no notification of the event is generated or sent. Since UDP does not provide any error-correction mechanisms, this event will go unnoticed, further contributing to UDP's unreliable nature.
When the UDP checksum is calculated, it also includes a pseudo-header in the calculations. The pseudo-header includes the source and destination IP addresses, as well as the Protocol Identifier (17 for UDP) and the size of the UDP message (including both the header and the data). The pseudo-header is combined with the UDP header and data segments, and a checksum is calculated. By including the pseudo-header in the calculations, the destination system is able to validate that the sender and receiver are also correct, in case the IP packet that delivered the UDP message got mixed up en route to the final destination.

Capture Sample
In the capture shown in Figure 6-7, the Checksum has been calculated to hexadecimal 76 ed, which is correct.

Troubleshooting UDP

Since UDP provides only simple delivery services, almost all of the problems with UDP are related to delivery problems—perhaps an expected application server is

0264-01.gif
Figure 6-7.
The Checksum field

unavailable, or is not running on the expected port number, or is no longer accepting messages for any number of other reasons.

ICMP Destination Unreachable: Port Unreachable Error Messages

In order to effectively debug problems with UDP delivery, you should rely on the ICMP protocol. It is the function of ICMP to report on problems that will keep IP datagrams (or UDP messages) from getting delivered to their destination effectively. For more information on ICMP, refer to Chapter 5, The Internet Control Message Protocol.

Figure 6-8 shows a problem with a TFTP client being unable to send data to a TFTP server. In this example, Greywolf has attempted to send data to Arachnid using TFTP. However, Arachnid is not running a TFTP server at this moment, so it returns an ICMP Destination Unreachable: Port Unreachable Error Message back to Greywolf.

If an application receives an ICMP error in response to an action, either it may try to send the data again or it may give up on the operation. Either way, any errorcorrection services must be implemented by the application, and not by UDP.

Another important point to make here is that many applications will not intercept or display ICMP Error Messages to the users of the application. Therefore, in order to diagnose problems such as this, it may be necessary to capture and decode the traffic on your network, using a tool such as Shomiti's Surveyor Lite.

0265-01.gif
Figure 6-8.
An ICMP Destination Unreachable: Port Unreachable Error Message

UDP-Based Application Failures.

Since UDP provides a datagram-based transport service that does not offer any of the reliability services found with TCP, UDP-based applications are more prone to failures in a noisy or loss-intensive network. If the network you are using to move IP packets across loses a lot of packets, then this failure will be felt with UDP-based applications before it is noticed by TCP-based protocols.

For example, Sun's Network File Service (NFS) typically uses UDP for remote filesystem access, since it benefits from the low-overhead nature of UDP. In addition, NFS typically writes data in large chunks (such as eight-kilobyte blocks), which are then split into multiple IP fragments according to the MTU characteristics of the underlying topology. Once all of the fragments have been received by the destination system, the IP datagram is reassembled and the UDP/NFS message is read and processed. However, if the underlying network loses 20% of the fragments, then the entire IP datagram will not be received (and thus the NFS data will not be processed).

With TCP-based applications, any data that gets lost will be recognized and retransmitted, resulting in slower overall performance, but a functional connection. With UDP however, lost data is gone forever and must be recognized and dealt with by the application. If the network constantly loses data however (such as is seen with sustained congestion levels or 20% packet loss due to line problems), then UDP-based applications that rely on fragmentation will constantly fail.

If you have problems with UDP-based applications such as NFS—but TCP-based applications like FTP work just fine—then you will need to investigate the loss characteristics of the network. You may need to reduce the amount of data that you are sending in a single operation (perhaps setting it equal to the MTU size or smaller), or you may need to use an application protocol that is TCP-based (many NFS implementations also support TCP).

Misconfigured or Missing Services File

You should also verify that the services file in use on your system matches up with the well-known port numbers expected by the application. Some applications will ask the system for the port number associated with TFTP, for example, and if your system's services file does not have an entry for that application, it will not return a port number to the client. This problem will prevent the client from being able to send any data, since it cannot get the destination port number for the application.

To see the well-known ports used on your system, examine the /etc/services file on a Unix host, or the C:\WinNT\System32\Drivers\Etc\SERVICES file on a Windows NT host.

Firewalls Blocking UDP Messages

Many network managers block all UDP traffic, with the exception of a few critical ports (such as DNS). If you are experiencing problems connecting to remote UDP applications, then you should investigate whether or not a remote firewall is blocking UDP traffic. Note that the problem could also be reversed, with your firewall blocking the return UDP traffic. In this case, your UDP traffic may be leaving the network just fine, but the packets coming back from the remote system may be getting discarded by your own firewall.

Datagrams Are Corrupted or Never Sent

Sometimes, you may notice that a system does not always send the UDP datagrams that you expect it to, or that the first UDP packet from a stream of data does not get sent. Typically, this situation is a result of a lack of entries in the sender's ARP cache for the destination system. Not having an entry, the system is forced to issue an ARP lookup for the destination system, and then wait for a response.

However, many TCP/IP implementations allow only one IP packet to be held in the ARP call-back queue for any given host. If during the act of issuing the ARP lookup the sender receives another out-bound packet for that system, then the first packet is likely to get cleared from the ARP queue in order to make room for the new packet. In this case, the first packet (the one that caused the lookup in the first place) will be destroyed.

This particular problem happens most often when multiple packets are quickly sent to a destination system, or when the size of the UDP or ICMP message exceeds the MTU of the local network, forcing IP to fragment the single message into multiple IP packets. In the latter case, IP will issue two or more ARP requests (depending on the number of fragments generated) for the same destination system, resulting in the ARP agent clearing all but the last fragment from the call-back queue immediately. The last fragment that was sent before an ARP response was received will be the first fragment to get sent on the wire.

When the destination system receives this fragment, it will eventually discard the incomplete IP datagram (possibly returning an ICMP Time Exceeded Error Message), although the amount of time it took for this error to occur (60 seconds is the most common value) may be longer than the client waited for. In that situation, the ICMP error message would not be seen by the client, since it had long since disconnected from UDP.

One way to solve this problem is to create static entries in the ARP cache for the destination system, allowing the sender to immediately send the IP fragments, rather than waiting for the ARP agent to conduct a query first. For more information on issues with the ARP cache, refer to The ARP Cache in Chapter 3, The Address Resolution Protocol.

This is just another reason why UDP should not be used for important data. If the data is important, you must use TCP to ensure that lost segments are recognized and eventually retransmitted.




Internet Core Protocols. The Definitive Guide with Cdrom
Internet Core Protocols: The Definitive Guide: Help for Network Administrators
ISBN: 1565925726
EAN: 2147483647
Year: 1999
Pages: 17
Authors: Eric Hall

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net