Setup TomP2P with Eclipse

If you are using Eclipse then these steps will setup a working version of TomP2P in Eclipse.

  1. Download Eclipse. The latest version is recommended
  2. Unzip and run it
  3. Click on “Help” -> “Install New Software...” -> select in “Work with:” the URL that has the word release in it (e.g. Indigo – http://download.eclipse.org/releases/indigo)
  4. Expand “Collaboration” and select “Eclipse EGit” and “m2e - Maven Integration for Eclipse”. Install those two plugins
  5. After restarting Eclipse, go to “Window” -> “Show View” -> “Git Repository Exploring”. Copy the URL “https://tbocek@github.com/tomp2p/TomP2P.git” and paste in the the Git Repository View (Paste Repository Path or URI). Click “next” -> select “master”, unselect the others -> next -> finish
  6. Right click on “TomP2P” in the repository view and click “Import Projects...” -> Select “Import as general project” -> next -> finsh
  7. Go to the Java perspective and click on “View Menu” (this is the triangle right next to minimize and maximize in the Package Explorer pane). Select “Filters...” and unselect “*.resources” -> ok
  8. Open .project and replate these empty XML tags in .project

<buildSpec>
</buildSpec>
<natures>
</natures>

with

<buildSpec>
  <buildCommand>
    <name>org.eclipse.jdt.core.javabuilder</name>
    <arguments>
    </arguments>
  </buildCommand>
  <buildCommand>
  <name>org.eclipse.m2e.core.maven2Builder</name>
  <arguments>
  </arguments>
  </buildCommand>
</buildSpec>
<natures>
  <nature>org.eclipse.m2e.core.maven2Nature</nature>
  <nature>org.eclipse.jdt.core.javanature</nature>
</natures>

Then right click on “TomP2P” -> “Maven” -> “Update Project Configuration...” -> ok. Now you are ready to go.

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.

  1. public FutureData send(final PeerAddress remotePeer, final Object object) throws IOException
  2. public FutureData send(final PeerAddress remotePeer, final ChannelBuffer requestBuffer)

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 p1 = null;
Peer p2 = null;
try
{
	p1 = new Peer(new Number160(rnd));
	p1.listen(4001, 4001);
	p2 = new Peer(new Number160(rnd));
	p2.listen(4002, 4002);
	//attach reply handler
	p2.setObjectDataReply(new ObjectDataReply()
	{
		@Override
		public Object reply(PeerAddress sender, Object request) throws Exception
		{
			System.out.println("request ["+request+"]");
			return "world";
		}
	});
	FutureData futureData=p1.send(p2.getPeerAddress(), "hello");
	futureData.awaitUninterruptibly();
	System.out.println("reply ["+futureData.getObject()+"]");
}
finally
{
	p1.shutdown();
	p2.shutdown();
}

The output of this example is:

request [hello]
reply [world]

TomP2P on Android

TomP2P has been tested and used on Anroid in a couple of academic projects. To install Android, please read the Quick Start. For any kind of development it is useful to use logcat for the error messages.

Extra Work

Although, the Android API is very similar to Java 1.5, you need to do some extra work.

  • Since the emulator does not support IPv6, you have to use System.setProperty("java.net.preferIPv6Addresses", "false"); before using TomP2P
  • Permissions: since TomP2P uses sockets, permissions have to be granted in the manfiest file. In Eclipse, double-click AndroidManifest.xml, open tab Permissions, add “Uses Permission” and select the permission “android.permission.Internet” in the drop-down menu. Make sure you have the following entry in AndroidManifest.xml <uses-permission android:name="android.permission.INTERNET"/>.
  • You can safely ignore the “warning: Ignoring InnerClasses attribute for an anonymous inner...”. Please see the following page for more information about this warning.

Example

The following example code has been tested on the latest Android 4.0.3 with TomP2P 4.0.7. Please note that the current TomP2P does not work with Android 2.3 or 2.2:

public class TestP2P extends Activity {
  final private static Random rnd = new Random(42L);
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
      
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        System.setProperty("java.net.preferIPv6Addresses", "false");
        Peer master = null;
        try
        {
                master = new Peer(new Number160(rnd));
                Bindings bindings=new Bindings();
                bindings.addProtocol(Protocol.IPv4);
                master.listen(4001, 4001, bindings);
                Peer[] nodes = createAndAttachNodes(master, 10);
                bootstrap(master, nodes);
                examplePutGet(nodes);
                exampleAddGet(nodes);
        }
        catch (Throwable e)
        {
          Log.wtf("tomp2p", e);
          e.printStackTrace();
        }
        finally
        {
                master.shutdown();
        }
        
    }
    private static void bootstrap(Peer master, Peer[] nodes)
    {
            List<FutureBootstrap> futures = new ArrayList<FutureBootstrap>();
            for (int i = 1; i < nodes.length; i++)
            {
                    FutureBootstrap tmp = nodes[i].bootstrap(master.getPeerAddress());
                    futures.add(tmp);
            }
            for (FutureBootstrap future : futures)
                    future.awaitUninterruptibly();
    }

    public static void examplePutGet(Peer[] nodes) throws IOException
    {
            Number160 nr = new Number160(rnd);
            String toStore = "hallo";
            Data data = new Data(toStore.getBytes());
            FutureDHT futureDHT = nodes[3].put(nr, data);
            futureDHT.awaitUninterruptibly();
            System.out.println("stored: " + toStore + " (" + futureDHT.isSuccess() + ")");
            futureDHT = nodes[7].get(nr);
            futureDHT.awaitUninterruptibly();
            //there are easier ways to get it, but this is for demonstration
            Data d1=futureDHT.getRawData().values().iterator().next().values().iterator().next();
            System.out.println("got: "
                            + new String(d1.getData(),d1.getOffset(),d1.getLength()) + " (" + futureDHT.isSuccess() + ")");
    }

    private static void exampleAddGet(Peer[] nodes) throws IOException
    {
            Number160 nr = new Number160(rnd);
            String toStore1 = "hallo1";
            String toStore2 = "hallo2";
            Data data1 = new Data(toStore1.getBytes());
            Data data2 = new Data(toStore2.getBytes());
            FutureDHT futureDHT = nodes[3].add(nr, data1);
            futureDHT.awaitUninterruptibly();
            System.out.println("added: " + toStore1 + " (" + futureDHT.isSuccess() + ")");
            futureDHT = nodes[5].add(nr, data2);
            futureDHT.awaitUninterruptibly();
            System.out.println("added: " + toStore2 + " (" + futureDHT.isSuccess() + ")");
            futureDHT = nodes[7].getAll(nr);
            futureDHT.awaitUninterruptibly();
            System.out.println("size " + futureDHT.getData().size());
            Iterator<Data> iterator = futureDHT.getData().values().iterator();
            Data d1=iterator.next();
            Data d2=iterator.next();
            System.out.println("got: " + new String(d1.getData(),d1.getOffset(),d1.getLength()) + " ("
                            + futureDHT.isSuccess() + ")");
            System.out.println("got: " + new String(d2.getData(),d2.getOffset(),d2.getLength()) + " ("
                            + futureDHT.isSuccess() + ")");
    }

    private static Peer[] createAndAttachNodes(Peer master, int nr) throws Exception
    {
            Peer[] nodes = new Peer[nr];
            nodes[0] = master;
            for (int i = 1; i < nr; i++)
            {
                    nodes[i] = new Peer(new Number160(rnd));
                    nodes[i].listen(master);
            }
            return nodes;
    }
}

The output of this example is for examplePutGet

stored: hallo (true)
got: hallo (true)

and for exampleAddGet

added: hallo2 (true)
size 2
got: hallo2 (true)
got: hallo1 (true)

The Eclispe workspace for TomP2P_Android is available for download: TomP2P-4.0.7_Android.zip (works with Android 4.0.3). An older version (works with Android 2.3) can be found here TomP2P-3.2.9_Android.zip.

Running and Connecting TomP2P on Two Emulators

To connect two TomP2P_Android applications and make them talk to each other, one has to setup port redirecting in Android. I’m not sure if this is needed, but I created two Android Virtual Devices (AVD): TomP2P-15 and TomP2P-15-1. Before setting up the port redirection, both AVD needs to be running (your-path-to-android-sdk/tools/emulator -avd TomP2P-15 and your-path-to-android-sdk/tools/emulator -avd TomP2P-15-1). The port redirection is set up by connecting to the device (telnet localhost 5554 and telnet localhost 5556) and run the commands redir add udp:5001:4001 and redir add tcp:5001:4001 for the emulator on port 5554 and redir add udp:6001:4001 / redir add tcp:6001:4001 for the emulator listening on port 5556. Now with the test application TestP2P, you should be able to connect from one emulator to the other by using the IP 10.0.2.2. The screenshot shows how the input (GUI) and output (console) should look like.

There is also a thread @stackoverflow.

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:

  • Number of direct hits (d): This is used for fetching data. If d peers have been contacted that have the data stored, routing stops.
  • Number of no new information (n): This is mainly used for storing data. It searches the closest peers and if n peers do not report any closer nodes, the routing stops.
  • Number of failures (f): The routing stops if f peers fail to respond.
  • Number of parallel requests (p): This tells the routing how many peers to contact in parallel.

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:

  • Number of the difference of parallel requests to the routing request ®. If you set parallel routing request p to 2 and r to 3, then 5 peers will be contacted for the get or put operation.
  • Number of minimum results (m): Stops the direct calls if m peers have been contacted.
  • Number of maximum failures (f): Stops the direct calls if f peers have failed.

DHT Configurations: ConfigurationStore

ConfigurationStore lets you configure:

  • Absent: Indicates if an existing value should be overwritten or not.
  • ProtectDomain: Indicates if the a domain should be protected from others to write to that domain.
  • RefreshSeconds: If set to > 0, the the store command will be called periodically until the peer removes this key.
  • setFutureCreate: A listener can be set that is called whenever a periodically command is issued.

DHT Configurations: ConfigurationRemove

Some of the options of ConfigurationRemove are the same as of ConfigurationStore. The differences are:

  • ReturnResults: Indicate if a remove should return the removed results.
  • Repetitions (x): The remove command is called x times.

DHT Configurations: ConfigurationDirect

Some of the options of ConfigurationDirect are the same as of ConfigurationStore / ConfigurationRemove. The differences are:

  • CancelOnFinish: Indicates if operations should terminate or be canceled. For storage the flag is set to false, for retrieval it set to true. For user command, the correct behavior is unknown and the user can set the flag.

DHT Configurations: ConfigurationGet

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: ConfigurationTrackerStore

The tracker configuration lets you configure:

  • Attachment: Besides the IP and PeerID, also data in an attachment can be stored on a tracker.
  • TrackerConfiguration is similar to RequestP2PConfiguration.
  • setFutureCreate: A listener can be set that is called whenever a periodically command is issued. TTL is set on the receiver side, which also sets the refresh time at the sender side.

Tracker Configurations: ConfigurationTrackerGet

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.

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:

  • NO_DOMAIN_MASTER or MASTER_DOMAIN_PUBLIC_KEY
  • DOMAIN_PROTECTION_ALL or DOMAIN_PROTECTION_NONE
  • removeDomainProtection(new Number160(“channel”))

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

  • NO_ENTRY_MASTER or MASTER_ENTRY_PUBLIC_KEY
  • ENTRY_PROTECTION_ALL or ENTRY_PROTECTION_NONE

The following modes exist for entries in a protected domain:

  • ENTRY_REMOVE_IF_DOMAIN_CLAIMED | ENTRY_LEAVE (default)

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:

  • DOMAIN_PROTECTION_NONE and MASTER_DOMAIN_PUBLIC_KEY: results in the behavior that nobody can protect a domain except the one with the master public key. The master key is if hash(public key) == domain.
  • DOMAIN_PROTECTION_NONE and NO_DOMAIN_MASTER: nobody can protect any domain
  • DOMAIN_PROTECTION_ALL and MASTER_DOMAIN_PUBLIC_KEY: all peers can protect domains, but the one with the public key can overwrite the protection. If this is the case and ENTRY_REMOVE_IF_DOMAIN_CLAIMED is set, all entries that do not belong to a public key are removed. If ENTRY_LEAVE is set, nothing happens to those entries.
  • For ENTRY modes the same rules apply, except ENTRY_REMOVE_IF_DOMAIN_CLAIMED and ENTRY_LEAVE has no effect.
  • The domain is always stronger than the entry, e.g., if there is MASTER_DOMAIN_PUBLIC_KEY, DOMAIN_PROTECTION_ALL, ENTRY_REMOVE_IF_DOMAIN_CLAIMED, MASTER_ENTRY_PUBLIC_KEY, and ENTRY_PROTECTION_ALL in place and peer b protects an entry x in domain y, then peer a, which has a hash of its public key equals y, can protect this domain and remove entry x from peer b.

Examples

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

  • protectionDomainMode = ProtectionMode.MASTER_PUBLIC_KEY;
  • protectionDomainEnable = ProtectionEnable.ALL;
  • protectionEntryMode = ProtectionMode.MASTER_PUBLIC_KEY;
  • protectionEntryEnable = ProtectionEnable.ALL
  • protectionEntryInDomain = ProtectionEntryInDomain.ENTRY_LEAVE;

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:

ConfigurationStore cs1 = Configurations.defaultStoreConfiguration();
cs1.setProtectDomain(true);
cs1.setDomain(new Number160(11));
FutureDHT fdht1 = peer_xy.put(locationKey, new Data("test1"), cs1);

The domain with the number 11 is now protected and only peer_xy 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 11, can overtake this domain. 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:


ConfigurationStore cs1 = Configurations.defaultStoreConfiguration();
cs1.setContentKey(new Number160(12))
Data data=new Data("test1");
data.setProtectedEntry(true);
FutureDHT fdht1 = peer_xy.put(locationKey, new Data("test1"), cs1);

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 code ConfigurationTrackerStore.setSign(true);.

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 ConfigurationStore.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 and copy 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

ConfigurationStore cs=Configurations.defaultStoreConfiguration();
cs.setRefreshSeconds(2); // greater than 0
cs.setFutureCreate(new FutureChain<FutureDHT>()
{
 @Override
 public void repeated(FutureDHT future)
 {
  System.err.println("chain...");
 }
});
FutureDHT fdht=nodes[1].put(nodes[50].getPeerID(),  new Data("test"), cs);
Utils.sleep(9*1000);
//outputs five times chain...

Indirect Replication

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

  • One event is if a peer x gets to know a new peer y. Then peer x checks all its stored data for which peer x is responsible for. If peer y is the new responsible for this stored data, the content will be handed over to peer y and peer y will be the new responsible peer. If peer y leaves the system that was responsible for content and peer x becomes the new responsible peer, peer x checks if there are enough replicas for this content. If not enough replicas are stored on peers, peer x starts to replicate the content.
  • The second event is triggered by a timer. Peer x checks periodically all stored data for enough replicas in the network. If not enough replicas are stored on peers, peer x starts to replicate the content.

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
{
 public Collection<Number160> put(Number160 locationKey, Number160 domainKey, PublicKey publicKey, Map<Number160, Data> contentMap, boolean putIfAbsent, boolean domainProtection)
 {
  if (PEER_VOTE.equals(domainKey))
  {
   //here we could get an int from the data and sum it up
  }
  else
   super.put(locationKey, domainKey, peerAddress, publicKey, contentMap, putIfAbsent, domainProtection);
 }
 ...
}

The second approach is recommended. For setting up the storage memory it is important to do this after the listen(...) method call.

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:

Peer peer = new Peer(new Number160(r));
peer.listen(4000, 4000);
PeerAddress pa = new PeerAddress(Number160.ZERO, boostrap IP, 4000, 4000);
peer.getP2PConfiguration().setBehindFirewall(true);
//find out how the other peers see me
FutureDiscover fd = peer.discover(pa);
fd.awaitUninterruptibly();
if (fd.isSuccess()) {
	System.out.println("found that my outside address is "+ fd.getPeerAddress());
} else {
	System.out.println("failed " + fd.getFailedReason());
}
peer.boostrap(...);