UPNP NAT and Port Forwarding detection

Since version 3.2.7, TomP2P supports port forwarding detection and port forwarding settings via UPNP and NAT-PMP. Such a port forwarding is important if peers are behind NAT to be reachable. If a peer is behind a NAT without port forwarding, the peer cannot participate in the P2P network.

Port Forwarding Detection

TomP2P can detect if there is already an existing port forwarding rule in place. First the peer behind the NAT needs to conact a well-known peer (typically the bootstrap peer). This peer reports back how the peer is seen behind the NAT. With this information the peer behind the NAT can guess how the port forwarding rule might look like. It is a guess, since the peer assumes that the internal and external port is the same. In cases where internal and external ports is not the same, there is the option to set the external port manually.

Automatic Port Forwarding Setting

With UPNP and NAT-PMP, TomP2P can set the port forwarding rules on the router automatically. This happens during the port forwarding detection. As soon as TomP2P knows how other peers see a peer, and the peer detects that it is behind a NAT, it tries to set the port forwarding. The configuration in TomP2P for the port forwarding allows to enable or disable it with P2PConfiguration.setBehindFirewall(). Since the default behavior to announced the external IP changed in 3.2.7, a peer needs to discover its external IP address before it can participate (boostrap) in the network:

Random rnd = new Random();
Peer peer = new PeerBuilder(new Number160(rnd)).ports(4001).start();
InetAddress address = Inet4Address.getByName("192.168.1.20");
FutureDiscover futureDiscover = peer.discover().inetAddress( address ).ports( 4000 ).start();
futureDiscover.awaitUninterruptibly();
if (fd.isSuccess()) {
	System.out.println("found that my outside address is "+ fd.peerAddress());
} else {
	System.out.println("failed " + fd.peerAddress());
}
peer.bootstrap().peerAddress(fd.peerAddress()).start();

Custom Commands

Routing is always performed for the built-in add/put/get methods in TomP2P. If a user wants to use its own commands together with the routing, the user can either use send(key, ...), which calls send(peer, ...) in the end. With this approach the user has to define what to reply with setRawDataReply() or setObjectDataReply() depending on whether an object is used or a ChannelBuffer. The second approach is to extend StorageMemory, e.g., with MyStorageMemory, and intercept the calls for a specific domain. The following example code shows such a custom behavior.

public class MyStorageMemory extends StorageMemory {
 @Override
 public Data put(Number640 key, Data value) {
  if (PEER_VOTE.equals(domainKey)) {
   //here we could get an int from the data and sum it up
  } else {
   return super.put(key, value);
  }
 }
}

Replication

In TomP2P, there are indirect and direct replication mechanisms available. The direct replication can be described as peers constantly publishing their content. This means that a single peer is responsible for its content and periodically republishes the content. If this peer stops doing so, the content will eventually time out and gets removed. The information about responsibilities need to be stored along with the data in the Storage class. The indirect replication can be described as peers publishing content for others. The peer closest to a location ID is considered as the responsible peer and replicates data if necessary.

Direct Replication

The direct replication can be turned on by setting DHTBuilder.setRefreshSeconds(int refreshSeconds) to a value greater than 0. This tells TomP2P to refresh the entry every refreshSeconds seconds. The information what content has to be refreshed is stored in the Storage class, which can be either memory or disk-based. The add and put methods are similar with respect to the direct replication except that add calculates a hash of the data on the sender, while put does not. The time-to-live (TTL) in combination with storage tells all other peers (except the sender) to invalidate the entry after this time, if no refresh has been performed. The sender never expires “its” content unless the sender calls remove.

The TTL and refreshSeconds for the remove operation has a different meaning. The refresh tells the sender to remove the entry every refreshSeconds until TTL has been reached. These values are used on the sender only.

The tracker does only direct replication and never active replication. It has a fixed timing, which can be set with TrackerStorageMemory.setTrackerTimoutMillis(TTL). for the TrackerStorage and its TTL * 0.75 for the refresh interval.

The following example executes 5 times put

PutBuilder putBuilder = peer.put(Number160.ONE).data(new Data("test"));
JobScheduler replication = new JobScheduler(peer.peer());
Shutdown shutdown = replication.start(putBuilder, 1000, -1, new AutomaticFuture() {
 @Override
 public void futureCreated(BaseFuture future) {
  System.out.println("put again...");
 }
});
Thread.sleep(NINE_SECONDS);
shutdown.shutdown();

Indirect Replication

Since version 3.2, TomP2P supports indirect replication. This feature can be enabled for put/get storage with DHTBuilder.setDefaultStorageReplication() and for tracker storage TrackerBuilder.setDefaultTrackerReplication(). Indirect replication is triggered by two events:

new IndirectReplication(peer).start();

Security in TomP2P

Security features are typically needed in uncontrolled environments, e.g., the Internet. The built-in security features of TomP2P (>= 3.1.10) are signature-based. Encryption-based security is not built-in, but a user can encrypt its data if necessary. Thus, the following sections explain how to use the signature-based security features. For more information about signing and encryption in P2P, please see Secure routing for structured peer-to-peer overlay networks.

Signatures

Two types of signatures exist in TomP2P: message signatures and data signatures. While the message signature signs the complete message including header (IP and peerID), the data signature only signs the data object.

Data Signatures

Any data objects can be signed. For example the following object Data data=new Data("content"); is signed with data.signAndSetPublicKey(keyPair);. Thus, the data can be signed with a different keys as the message signature. As the name signAndSetPublicKey suggests, the data object is signed and the public key is attached (self-signature). A recipient of this data object can verify if the data object is signed with public key provided and compare the received public key with already stored public keys. A public key is stored on first contact and any subsequent storage, removal, etc. can be verified if its the same peer.

Data signatures are typically used in the Internet with active replication, where peers move around data objects. A message signature would not work in such a situation, since a message signature always includes the IP and the peerID of the sender, and this can change with active replication.

Message Signatures

Message signatures sign the complete message. Messages that can be signed are remove, add, store, move, get, and copy messages. These signatures are used to protect entries, excpet the get message. An example scenario, where such a protection is necessary, is in a tracker, where peers announces its availability of a file. For non-protected entries, anyone could overwrite those entries making an attack easy. Thus, the protection works that the entry can only be modified by the original author. Data and message signatures have been separated since signed data objects that should replace signed messages would require additional mechanisms such as message type indication, or peerID identification, etc.

Domain and Entry Protection Mechanisms

With signed messages, domains and entries can be protected. Protected means, that nobody else can overwrite it.

The following modes and methods exist for domains and entries:

The following modes and methods exist for entries in unprotected domains:

The following modes exist for entries in a protected domain:

If a protected domain has no entries, i.e., they all expired, the domain gets unprotected. The following table describes those modes and methods.

NO_*_MASTER or MASTER_*_PUBLIC_KEYThis mode, in both domain and entry specifies, if a hash of the public key of a signed message can overwrite a currently preset value. NO_MASTER cannot overwrite, while MASTER_PUBLIC_KEY can, even if the domain is protected.
DOMAIN_PROTECTION_ALL or DOMAIN_PROTECTION_NONEThis modes either sets all domains, which can be protected by any peer, or none.
removeDomainProtection(x)This methods removes domains, which can be protected. The opposite addDomainProtection does not make much sense here, because every peer has to specify it and is publicly known, which makes it a perfect target for malicious peers.
ENTRY_PROTECTION_ALL or ENTRY_PROTECTION_NONEThis mode turns of or on protection for entries.

These combinations are possible:

Examples

These modes are set by default, which should be fine for most applications.

If other values should be set, e.g., disable domain protection, use setProtection(protectionDomainEnable, protectionDomainMode, protectionEntryEnable, protectionEntryMode, protectionEntryInDomain).

A peer can protect a domain as follows:

peer1.storageLayer().protection(ProtectionEnable.ALL, ProtectionMode.MASTER_PUBLIC_KEY, 
  ProtectionEnable.ALL, ProtectionMode.MASTER_PUBLIC_KEY);
FutureDHT futureDHT = peer1.put(Number160.ONE).setData(new Data("test"))
  .setProtectDomain().setDomainKey(Number160.ZERO).start();

The domain with the number peer2Owner is now protected and only peer1 can use this domain for data storage for any content key if the master public key mode is not set. If it is set, the peer with the hash of the public key equals 0, can overtake this domain as shown in the next example:

KeyPairGenerator gen = KeyPairGenerator.getInstance( "DSA" );
KeyPair pair2 = gen.generateKeyPair();
final Number160 peer2Owner = Utils.makeSHAHash( pair2.getPublic().getEncoded()); // results in 0
FuturePut futurePut = peer1.put(Number160.ONE).data(new Data("test"))
  .domainKey(peer2Owner).protectDomain().start();

Domains can also be set to never be protected with storage.removeDomainProtection(new Number160(11));. Also, a public key with this hash cannot protect this domain if the domain is set to unprotectable. The entry protection works similar:

Data data=new Data("test1");
        data.protectEntry();
FuturePut futurePut = peer1.put(Number160.ONE).data(data)
  .protectDomain().domainKey(peer2Owner).start();

The entry with the key 12 can only be changed by the peer_xy if the master content key mode is not set.

Tracker Security

A tracker has the built in modes: NO_DOMAIN_MASTER, DOMAIN_PROTECTION_NONE, ENTRY_PROTECTION_NONE, MASTER_ENTRY_PUBLIC_KEY, which means that if the peer decides to protect its entry on the tracker, the key has to be the hash of its public key and the add message has to be signed. This can be done by calling TrackerBuilder.setSign(true);.

Configurations

Each DHT operation in TomP2P can be configured. All configurations extend ConfigurationBase, which lets you set the content key (default 0), the domain (default “P2P domain”), the routing configuration and if the message should be signed. Reasonable defaults are created when using the Configurations class. The routing configuration lets you configure in particular:

Furthermore, each DHT configuration has a RequestP2PConfiguration, which configures the direct calls, such as get or put. The P2P configuration lets you configure in particular:

DHT Configurations: PutBuilder

PutBuilder lets you configure:

DHT Configurations: RemoveBuilder

Some of the options of RemoveBuilder are the same as of PutBuilder. The differences are:

DHT Configurations: SendDirectBuilder

Some of the options of SendDirectBuilder are the same as of PutBuilder / RemoveBuilder. The differences are:

DHT Configurations: GetBuilder

Since multiple results can be returned, an evaluation of the results is necessary. Such a result evaluation needs to implement EvaluatingSchemeDHT. The default scheme is a majority voting. Furthermore, the retrieval can specify a public key to get data from a specify peer only.

Tracker Configurations: AddTrackerBuilder

The AddTrackerBuilder configuration lets you configure:

Tracker Configurations: GetTrackerBuilder

Similar to the DHT get operation, an evaluation of the results is necessary. The flag expectAttachement has to be set if the caller is interested in the attachment.

Direct Messages

TomP2P can send direct messages in an RPC-style to other peers. There are two types of send() functions defined in TomP2P: sending objects and sending raw data. While the first serializes the object to a byte array, the later one does not and should be used if there is alreay a byte array.

For the usage of ChannelBuffer, please refer to this http://docs.jboss.org/netty/3.2/api/org/jboss/netty/buffer/class-use/ChannelBuffer.html. Basically, to create a ChannelBuffer from a byte[], you can use e.g., ChannelBuffers.wrappedBuffer(byte[] array) as described http://docs.jboss.org/netty/3.2/api/org/jboss/netty/buffer/ChannelBuffers.html.

Example

The following example shows how to send objects that can be serialized to an other peer.

peer.peer().objectDataReply(new ObjectDataReply() {
 @Override
 public Object reply(PeerAddress sender, Object request) throws Exception {
  System.err.println("I'm " + peer.peerID() + " and I just got the message [" + request
    + "] from " + sender.peerId());
  return "world";
 }
});