Traversing Firewalls and NATsNotesAs almost everybody is aware, the Internet is partially partitioned by the presence of firewalls and Network Address Translation devices. Many hosts do not have a globally visible IP address on any network interface and instead have private network address that are translated to and from a global address via an intermediate host usually referred to as a firewall or NAT router. This usually isn't a problem for an outgoing connection as the intermediates are designed to handle this situation transparently. However, an incoming connection is a problem. The intermediate gets the connection...and it has no idea that the connection should be forwarded to an interface on its other side. There are a number of solutions to this problem. You can manually configure the intermediate to recognize that an incoming connection on a certain port should be forwarded to a service on another host. There are also several systems for having service software configure the intermediates automatically. They unfortunately aren't inter-compatible nor standardized. (http://en.wikipedia.org/wiki/NAT_traversal) In simplified TeaTime the only service that gets incoming TCP connections is the message router. All of the island replicates only generate outgoing TCP connections to the router. In the variation used by Wisconsin there are two islands, one for the scene graph and another for content distribution. On a LAN there is a contact point system (CRQT) that broadcasts over UDP the appropriate connection information, the router IP, port number, and the UUIDs of the capabilities that identify the specific island, and the scene graph object to use as a teleport destination. Note that the router does not have to run on the same host as any of the island replicates. We must have a manual configuration option as some intermediates don't have any automatic means of configuring NAT (or this my be prohibited by the security policy) and some of the more complex configurations won't work with the automatic systems. Many of the pieces of the manual configuration system can serve as a building blocks for more automatic schemes. Just for clarity and generality, we'll
assume that the router is being run on another host. This implies that
the router will have to either handle the NAT traversal and and
communicate the appropriate address information back to the initial
connecting island so it can generate the contact point or it will do
all the contact point handling itself when a new island connects and
registers. Either scenario can work, more research is required into finding the optimal approach. Thus, we need some way to tell Open Cobalt that there are potentially two kinds of contact points (or perhaps extend the contact points to have both addresses and ports). If the request for a contact comes from a host NOT on the local private network (or the intermediate?), the postcards/contact points it hands out for itself should have the address information for the intermediate rather that the local host. This will also require a way of fixing the port number of the cobalt message router at start-up. This should be pretty easy to do and should be the first thing do do in this area. Manual configuration alone will be adequate for an early release which will be looked at by competent developers. There should be some preferences that the NAT traversal configuration can be stored in. The
automatic configuration options require some more thought as it is not
obvious which approach should be first. We might want to survey our
user base or write detection software and gather some statistics about
which automatic system is more common to get the most effective use of
our time. But once a manual system is working it should be
straightforward (if time-consuming) to have an automatic configuration
mode.
ImplementationFinding the routerAfter digging around for a bit, it appears that the most widely used automatic NAT traversal option for TCP is the Internet Gateway Device service set (IGD) of Universal Plug and Play (UPnP). NAT-PMP is in use, but on a much more limited scale, and many NAT-PMP routers also support UPnP-IGD. SOCKS is also still around, but gradually going away. I would prefer the IETF protocols (http://www.zeroconf.org/ZeroconfAndUPnP.html), and perhaps I'll implement them later. So I (jdougan) have implemented a UPnP stack that has enough support to work with the IGD. As UPnP uses SOAP for controlling the devices and writing a SOAP stack is a PITA, I've just used the SOAPCore client implementation for that aspect. It's a bit large just for this, but we can find or build something lighter weight later. For that matter, now we have a SOAP client, we may find more useful things for it to do. The UPnP package, which is still under development, is at the Cobalt mc (Monticello) server in the contributions project. Besides SOAPCore, it also uses the RBT package to manage the UDP sockets. Both of these are available on SqueakMap and by now the updated versions with underscore assignments removed should be available. To use the UPnP stack, first you must start the announcement protocol (SSDP):
or
Then we can get a reference to a WANIPConnection IGD service, which is the service that lets us manipulate port forwarding mappings. Here we just just grab one at random which will work in most cases because there is only one. If you have more than one NAT router visible you'll have to make a more reasoned decision or manipulate all of them in parallel. wanIpConnServ := SsdpListener wanIpConnectionServices anyOne. And now we can manipulate the router: "Get a OrderedCollection of descriptive OCs giving the port mapping info in a canonical order" "Order is: { Remote Host. External Port. Protocol, Internal Port. Internal Client. Enabled?. Description. Lease Duration}" "eventually this will probably get a class of it's own." wanIpConnServ getPortMappings. "External IP Address as a string" wanIpConnServ getExternalIPAddress. "Add a port mapping"
wanIpConnServ addPortMapping: 'Description String' remoteHost: remoteHostIPString " '' is a wildcard and the usual value" externalPort: externalPort protocol: protocol "TCP or UDP" internalHost: internalHostIPString internalPort: internalPort enabled: isEnabled "should be a 1 or a 0" leaseDuration: leaseDuration "0 means unlimited duration" "Abbreviated way of adding a port mapping that defaults many of the values" "Most common options set: wildcard remotehost, tcp protocol, enabled, infinite lease" wanIpConnServ addPortMapping: desc externalPort: externalPort internalHost: internalHost internalPort: internalPort "Delete a port mapping"
wanIpConnServ deletePortMappingHost: remoteHost externalPort: externalPort protocol: protocol "alternate method of deleting a port mapping, argument is one of the descriptive arrays from #getPortMappings" wanIpConnServ deletePortMapping: descArray DispatchersI decided to put as much of the NAT Traversal functionality as possible with the dispatcher and routers. The major reason for this was because we will probably eventually have standalone dispatcher/router sets (or even a completely different P2P routing architecture) so we can't depend on having anything other than the dispatcher, the routers and their support code. In the process of getting there, I've dumped using the TExampleDispatcher. Instead I've merged the code in TExampleDispatcher and KSDKDispatcher and put the result into the new TSessionDispatcher (so named because it associates session ids with routers). From there I've subclassed a CobaltDispatcher which has an understanding of the port forwarding and configuration. Configuration is handled by the tree of classes rooted in TRouterConfig (I'll probably rename this). They will activate the appropriate port forwarding and tell the dispatcher the resulting information needed. They also tell the dispatcher what local port to use if a specific one is required. CobaltDispatcher newWithConfig: (TLanOnlyRouterConfig new) will
create a new cobalt dispatcher with the configuration necessary to run
on the LAN only. This is essentially the policy used in the demos. CobaltDispatcher newWithConfig: ( (TManualRouterConfig new) internalPort: 12000 ; externalPort: 12001 ; externalIp: '123.45.67.89' ; yourself ) Will return a CobaltDispatcher configured to assume that there is port forwarding already manually configured with the parameters described. CobaltDispatcher newWithConfig: (TUpnpRouterConfig new) will return a CobaltDispatcher configured to handle port forwarding automatically via an available UPnP Internet Gateway Device. The CobaltDispatcher has a class variable DefaultConfig that holds a configuration to be used if the argument to #newWithConfig: is nil (oops, missed integration). In the current version (on 20080525) the CobaltHarness holds a configuration to be used on dispatcher startup and the dispatcher is created in #startDispatcher. The configuration used is in the class variable RouterPrefs on the CobaltHarness. This whole thing desperately needs some UI.
Contact PointNow we have the port forwarding in place, we have a new problem. The dispatcher potentially has 2 addresses: the internal to the private network address (usually the address of the local network interface) and the external network address (usually the address of the Internet side of the NAT router). Most of the code in the system is assuming that there is only one address....which puts me deep into refactoring land. In the interim, I've cleaned up TContactPoint a bit and subclassed a CobaltContactPoint off of it. The CobaltContactPoint understands that there is possibly an external address and will have additional functionality to tell some registry about it. It's currently only half done right now. The problem with all of this so far is that it assumes that you have unfettered access to the dispatcher, so you can call it and get the various bits of addressing information out of it. In many real production cases this will not be true as the dispatcher will be on a remote machine, so eventually there will have to be some network protocol between the island/harness/contact point and the dispatcher, probably via the Croquet datagrams in the same fashion as the router is currently communicated with. |
Documentation > Developer Resources >