package org.jgroups.protocols;
import java.io.DataInputStream;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.jgroups.Address;
import org.jgroups.Event;
import org.jgroups.Global;
import org.jgroups.Message;
import org.jgroups.PhysicalAddress;
import org.jgroups.annotations.Experimental;
import org.jgroups.annotations.Property;
import org.jgroups.stack.GossipData;
import org.jgroups.stack.GossipRouter;
import org.jgroups.stack.RouterStubManager;
import org.jgroups.stack.IpAddress;
import org.jgroups.stack.Protocol;
import org.jgroups.stack.RouterStub;
import org.jgroups.util.Buffer;
import org.jgroups.util.ExposedByteArrayOutputStream;
import org.jgroups.util.ExposedDataOutputStream;
import org.jgroups.util.Util;
/**
* Replacement for UDP. Instead of sending packets via UDP, a TCP connection is opened to a Router
* (using the RouterStub client-side stub), the IP address/port of which was given using channel
* properties <code>router_host</code> and <code>router_port</code>. All outgoing traffic is sent
* via this TCP socket to the Router which distributes it to all connected TUNNELs in this group.
* Incoming traffic received from Router will simply be passed up the stack.
*
* <p>
* A TUNNEL layer can be used to penetrate a firewall, most firewalls allow creating TCP connections
* to the outside world, however, they do not permit outside hosts to initiate a TCP connection to a
* host inside the firewall. Therefore, the connection created by the inside host is reused by
* Router to send traffic from an outside host to a host inside the firewall.
*
* @author Bela Ban
* @author Vladimir Blagojevic
*/
@Experimental
public class TUNNEL extends TP {
/*
* ----------------------------------------- Properties
* --------------------------------------------------
*/
@Property(description = "Interval in msec to attempt connecting back to router in case of torn connection. Default is 5000 msec")
private long reconnect_interval = 5000;
@Property(description="Should TCP no delay flag be turned on")
boolean tcp_nodelay=false;
/*
* --------------------------------------------- Fields
* ------------------------------------------------------
*/
private final List<InetSocketAddress> gossip_router_hosts = new ArrayList<InetSocketAddress>();
private TUNNELPolicy tunnel_policy = new DefaultTUNNELPolicy();
private DatagramSocket sock;
private volatile RouterStubManager stubManager;
public TUNNEL() {
}
public boolean supportsMulticasting() {
return false;
}
@Property
public void setGossipRouterHosts(String hosts) throws UnknownHostException {
gossip_router_hosts.clear();
// if we get passed value of List<SocketAddress>#toString() we have to strip []
if (hosts.startsWith("[") && hosts.endsWith("]")) {
hosts = hosts.substring(1, hosts.length() - 1);
}
gossip_router_hosts.addAll(Util.parseCommaDelimitedHosts2(hosts, 1));
}
public String toString() {
return "TUNNEL";
}
public long getReconnectInterval() {
return reconnect_interval;
}
public void setReconnectInterval(long reconnect_interval) {
this.reconnect_interval = reconnect_interval;
}
/*------------------------------ Protocol interface ------------------------------ */
public synchronized void setTUNNELPolicy(TUNNELPolicy policy) {
if (policy == null)
throw new IllegalArgumentException("Tunnel policy has to be non null");
tunnel_policy = policy;
}
public void init() throws Exception {
super.init();
if(enable_bundling) {
log.warn("bundling is currently not supported by TUNNEL; bundling is disabled");
enable_bundling=false;
}
if (timer == null)
throw new Exception("timer cannot be retrieved from protocol stack");
// Postpone TUNNEL and shared transport until 3.0 timeframe
// TODO [JGRP-1194] - Revisit implementation of TUNNEL and shared transport
if (isSingleton()) {
throw new Exception("TUNNEL and shared transport mode are not supported!");
}
if (log.isDebugEnabled())
log.debug("GossipRouters are:" + gossip_router_hosts.toString());
stubManager = RouterStubManager.emptyGossipClientStubManager(this);
sock = getSocketFactory().createDatagramSocket(Global.TUNNEL_UCAST_SOCK, bind_port, bind_addr);
// loopback turned on is mandatory
loopback = true;
}
public void destroy() {
stubManager.destroyStubs();
super.destroy();
}
private void disconnectStub(String group, Address addr) {
stubManager.disconnectStubs();
}
public Object handleDownEvent(Event evt) {
Object retEvent = super.handleDownEvent(evt);
switch (evt.getType()) {
case Event.CONNECT:
case Event.CONNECT_WITH_STATE_TRANSFER:
case Event.CONNECT_USE_FLUSH:
case Event.CONNECT_WITH_STATE_TRANSFER_USE_FLUSH:
String group=(String)evt.getArg();
Address local= null;
if(!isSingleton()) {
local = local_addr;
} else {
// TODO [JGRP-1194] - Revisit implementation of TUNNEL and shared transport
ProtocolAdapter adapter = ProtocolAdapter.thread_local.get();
local = adapter.local_addr;
}
stubManager = new TUNNELStubManager(this,group,local,getReconnectInterval());
for (InetSocketAddress gr : gossip_router_hosts) {
RouterStub stub = stubManager.createAndRegisterStub(gr.getHostName(), gr.getPort(), bind_addr);
stub.setTcpNoDelay(tcp_nodelay);
}
PhysicalAddress physical_addr=(PhysicalAddress)down(new Event(Event.GET_PHYSICAL_ADDRESS, local));
List<PhysicalAddress> physical_addrs=Arrays.asList(physical_addr);
String logical_name=org.jgroups.util.UUID.get(local);
List<RouterStub> stubs = stubManager.getStubs();
tunnel_policy.connect(stubs, group, local, logical_name, physical_addrs);
break;
case Event.DISCONNECT:
if(!isSingleton()) {
local = local_addr;
group = channel_name;
} else {
// TODO [JGRP-1194] - Revisit implementation of TUNNEL and shared transport
ProtocolAdapter adapter = ProtocolAdapter.thread_local.get();
local = adapter.local_addr;
group = adapter.cluster_name;
}
disconnectStub(group,local);
break;
}
return retEvent;
}
private class TUNNELStubManager extends RouterStubManager {
TUNNELStubManager(Protocol owner, String channelName, Address logicalAddress, long interval) {
super(owner, channelName, logicalAddress, interval);
}
public void connectionStatusChange(RouterStub stub, RouterStub.ConnectionStatus newState) {
super.connectionStatusChange(stub, newState);
if (newState == RouterStub.ConnectionStatus.CONNECTED) {
StubReceiver stubReceiver = new StubReceiver(stub);
stub.setReceiver(stubReceiver);
Thread t = global_thread_factory.newThread(stubReceiver, "TUNNEL receiver for " + stub.toString());
stubReceiver.setThread(t);
t.setDaemon(true);
t.start();
}
}
}
public class StubReceiver implements Runnable {
private Thread runner;
private final RouterStub stub;
public StubReceiver(RouterStub stub) {
this.stub = stub;
}
public synchronized void setThread(Thread t) {
runner = t;
}
public synchronized Thread getThread() {
return runner;
}
public void run() {
final DataInputStream input = stub.getInputStream();
mainloop:
while (!Thread.currentThread().isInterrupted()) {
try {
GossipData msg = new GossipData();
msg.readFrom(input);
switch (msg.getType()) {
case GossipRouter.DISCONNECT_OK:
break mainloop;
case GossipRouter.MESSAGE:
byte[] data = msg.getBuffer();
receive(null/* src will be read from data */, data, 0, data.length);
break;
case GossipRouter.SUSPECT:
final Address suspect = Util.readAddress(input);
log.debug("Firing suspect event " + suspect + " at " + local_addr);
if(suspect != null) {
// https://jira.jboss.org/jira/browse/JGRP-902
Thread thread = getThreadFactory().newThread(new Runnable() {
public void run() {
fireSuspectEvent(suspect);
}
}, "StubReceiver-suspect");
thread.start();
}
break;
}
}catch (Exception ioe) {
if(stub.isConnected())
continue mainloop;
else
break;
}
}
}
private void fireSuspectEvent(Address suspect) {
up(new Event(Event.SUSPECT, suspect));
}
}
protected void send(Message msg, Address dest, boolean multicast) throws Exception {
// we don't currently support message bundling in TUNNEL
TpHeader hdr=(TpHeader)msg.getHeader(this.id);
if(hdr == null)
throw new Exception("message " + msg + " doesn't have a transport header, cannot route it");
String group=hdr.channel_name;
ExposedByteArrayOutputStream out_stream=new ExposedByteArrayOutputStream((int)(msg.size() + 50));
ExposedDataOutputStream dos=new ExposedDataOutputStream(out_stream);
writeMessage(msg, dos, multicast);
Buffer buf=new Buffer(out_stream.getRawBuffer(), 0, out_stream.size());
if(stats) {
num_msgs_sent++;
num_bytes_sent+=buf.getLength();
}
List<RouterStub> stubs = stubManager.getStubs();
if(multicast) {
tunnel_policy.sendToAllMembers(stubs, group, buf.getBuf(), buf.getOffset(), buf.getLength());
}
else {
tunnel_policy.sendToSingleMember(stubs, group, dest, buf.getBuf(), buf.getOffset(), buf.getLength());
}
}
public void sendMulticast(byte[] data, int offset, int length) throws Exception {
throw new UnsupportedOperationException("sendMulticast() should not get called on TUNNEL");
}
public void sendUnicast(PhysicalAddress dest, byte[] data, int offset, int length) throws Exception {
throw new UnsupportedOperationException("sendUnicast() should not get called on TUNNEL");
}
public String getInfo() {
List<RouterStub> stubs = stubManager.getStubs();
if (stubs.isEmpty())
return stubs.toString();
else
return "RouterStubs not yet initialized";
}
protected PhysicalAddress getPhysicalAddress() {
return sock != null ? new IpAddress(bind_addr, sock.getLocalPort()) : null;
}
public interface TUNNELPolicy {
public void connect(List<RouterStub> stubs, String group, Address addr, String logical_name, List<PhysicalAddress> phys_addrs);
public void sendToAllMembers(List<RouterStub> stubs, String group, byte[] data, int offset, int length) throws Exception;
public void sendToSingleMember(List<RouterStub> stubs, String group, Address dest, byte[] data, int offset,
int length) throws Exception;
}
private class DefaultTUNNELPolicy implements TUNNELPolicy {
public void sendToAllMembers(List<RouterStub> stubs, String group, byte[] data, int offset, int length)
throws Exception {
boolean sent = false;
if(stubs.size() > 1)
Collections.shuffle(stubs); // todo: why is this needed ?
for (RouterStub stub : stubs) {
try {
if(!stub.isConnected())
continue;
stub.sendToAllMembers(group, data, offset, length);
if (log.isTraceEnabled())
log.trace("sent a message to all members, GR used " + stub.getGossipRouterAddress());
sent = true;
break;
} catch (Exception e) {
if (log.isWarnEnabled())
log.warn("failed sending a message to all members, GR used " + stub.getGossipRouterAddress());
}
}
if (!sent)
throw new Exception("None of the available stubs " + stubs + " accepted a multicast message");
}
public void sendToSingleMember(List<RouterStub> stubs, String group, Address dest, byte[] data, int offset, int length) throws Exception {
boolean sent = false;
if(stubs.size() > 1)
Collections.shuffle(stubs);
for (RouterStub stub : stubs) {
try {
if(!stub.isConnected())
continue;
stub.sendToMember(group, dest, data, offset, length);
if (log.isDebugEnabled())
log.debug("sent a message to " + dest + ", GR used " + stub.getGossipRouterAddress());
sent = true;
break;
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("failed sending a message to " + dest + ", GR used " + stub.getGossipRouterAddress());
}
}
}
if (!sent)
throw new Exception("None of the available stubs " + stubs
+ " accepted a message for dest " + dest);
}
public void connect(List<RouterStub> stubs, String group, Address addr, String logical_name, List<PhysicalAddress> phys_addrs) {
for (RouterStub stub : stubs) {
try {
stub.connect(group, addr, logical_name, phys_addrs);
}
catch (Exception e) {
if (log.isWarnEnabled())
log.warn("Failed connecting to GossipRouter at " + stub.getGossipRouterAddress());
stubManager.startReconnecting(stub);
}
}
}
}
}