package javaforce.service;
/** STUN (w/ TURN support) server.
*
* Created : Dec 24, 2013
*
* See RFCs:
* http://tools.ietf.org/html/rfc3489 - Classic STUN
* http://tools.ietf.org/html/rfc5389 - STUN
* http://tools.ietf.org/html/rfc5766 - TURN
* http://tools.ietf.org/html/rfc5245 - ICE
*
* Notes:
* - Doesn't support CHANGE_REQUEST w/ Different IP
* - bind & alloc use the same timer (refresh one, the other is refreshed too)
* - limit one channel per client ip/port
*/
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.net.*;
import java.nio.*;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javaforce.*;
import javaforce.jbus.JBusClient;
import javaforce.jbus.JBusServer;
public class STUN {
private static int defaultLifeTime = 600;
public final static String busPack = "net.sf.jfstun";
public static String getConfigFile() {
return JF.getConfigPath() + "/jfstun.cfg";
}
public static String getLogFile() {
return JF.getLogPath() + "/jfstun.log";
}
//requests
private final static short BINDING_REQUEST = 0x0001;
private final static short ALLOCATE_REQUEST = 0x0003;
private final static short REFRESH_REQUEST = 0x0004;
private final static short BIND_REQUEST = 0x0009;
//responses (success)
private final static short BINDING_RESPONSE = 0x0101;
private final static short ALLOCATE_RESPONSE = 0x0103;
private final static short REFRESH_RESPONSE = 0x0104;
private final static short BIND_RESPONSE = 0x0109;
//responses (failure)
private final static short BINDING_FAILED = 0x0111;
private final static short ALLOCATE_FAILED = 0x0113;
private final static short REFRESH_FAILED = 0x0114;
private final static short BIND_FAILED = 0x0119;
//attrs
private final static short MAPPED_ADDRESS = 0x0001;
private final static short CHANGE_REQUEST = 0x0003;
private final static short USERNAME = 0x0006;
private final static short MESSAGE_INTEGRITY = 0x0008;
private final static short ERROR_CODE = 0x0009;
private final static short CHANNEL_NUMBER = 0x000c;
private final static short LIFETIME = 0x000d;
private final static short XOR_PEER_ADDRESS = 0x0012;
private final static short REALM = 0x0014;
private final static short NONCE = 0x0015;
private final static short XOR_RELAY_ADDRESS = 0x0016;
private final static short DATA_INDICATION = 0x0017;
private final static short EVEN_PORT = 0x0018;
private final static short TRANSPORT_TYPE = 0x0019;
private final static short XOR_MAPPED_ADDRESS = 0x0020;
private final static short RESERVATION_TOKEN = 0x0022;
private volatile boolean active = true;
private volatile boolean done = false;
private String user, pass;
private DatagramSocket ds, ds2;
private final String realm = "javaforce.service.STUN";
private String publicip;
private boolean doStart() {
try {
JFLog.log("Starting STUN/TURN Service on ports 3478 and 3479");
ds = new DatagramSocket(3478);
ds2 = new DatagramSocket(3479);
new Worker().start();
return true;
} catch (Exception e) {
JFLog.log(e);
return false;
}
}
/** Starts a STUN/TURN server, loading config from file. */
public boolean start() {
JFLog.init(getLogFile(), true);
loadConfig();
busClient = new JBusClient(busPack, new JBusMethods());
busClient.setPort(getBusPort());
busClient.start();
return doStart();
}
/** Starts a STUN/TURN server with specific config options. */
public boolean start(String user, String pass, int min, int max) {
this.user = user;
this.pass = pass;
this.min = min;
this.max = max;
this.next = min;
return doStart();
}
enum Section {None, Global};
private final static String defaultConfig
= "[global]\n"
+ "## remove comments below and change as desired.\n"
+ "#user=\n"
+ "#pass=\n"
+ "#publicip=1.2.3.4\n"
+ "## min/max are the UDP port ranges to use\n"
+ "#min=10000\n"
+ "#max=20000\n"
;
private void loadConfig() {
Section section = Section.None;
try {
BufferedReader br = new BufferedReader(new FileReader(getConfigFile()));
StringBuilder cfg = new StringBuilder();
while (true) {
String ln = br.readLine();
if (ln == null) break;
cfg.append(ln);
cfg.append("\n");
ln = ln.trim().toLowerCase();
int idx = ln.indexOf('#');
if (idx != -1) ln = ln.substring(0, idx).trim();
if (ln.length() == 0) continue;
if (ln.equals("[global]")) {
section = Section.Global;
continue;
}
switch (section) {
case Global:
if (ln.startsWith("user=")) user = ln.substring(5);
else if (ln.startsWith("pass=")) pass = ln.substring(5);
else if (ln.startsWith("publicip=")) publicip = ln.substring(9);
else if (ln.startsWith("min=")) min = JF.atoi(ln.substring(4));
else if (ln.startsWith("max=")) max = JF.atoi(ln.substring(4));
break;
}
}
config = cfg.toString();
} catch (FileNotFoundException e) {
//create default config
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(defaultConfig.getBytes());
fos.close();
config = defaultConfig;
} catch (Exception e2) {
JFLog.log(e2);
}
} catch (Exception e) {
JFLog.log(e);
}
}
public int getLocalPort() {
return ds.getLocalPort();
}
public String getLocalAddr() {
return ds.getLocalAddress().getHostAddress();
}
public void close() {
active = false;
if (ds != null) {
try {
ds.close();
ds = null;
} catch (Exception e) {
JFLog.log(e);
}
}
if (ds2 != null) {
try {
ds2.close();
ds2 = null;
} catch (Exception e) {
JFLog.log(e);
}
}
while (!done) {
JF.sleep(10);
}
}
private byte[] calcKey() {
String msg = user + ":" + realm + ":" + pass;
MD5 md5 = new MD5();
md5.init();
md5.add(msg.getBytes(), 0, msg.length());
return md5.done();
}
private byte[] calcMsgIntegrity(byte data[], int length) {
byte key[] = calcKey();
try {
SecretKeySpec ks = new SecretKeySpec(key, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(ks);
return mac.doFinal(Arrays.copyOfRange(data, 0, length));
} catch (Exception e) {
JFLog.log(e);
return null;
}
}
private class Alloc {
String nonce;
String ip;
InetAddress addr; //same as ip
int port;
Timer timer;
DatagramSocket ds;
Object lock = new Object();
long timeout;
String id;
int relayip, relayport;
InetAddress relayaddr;
RelayWorker worker;
short channel;
boolean evenPort;
void setTimeout(int secs) {
synchronized(lock) {
if (timer != null) {
timer.cancel();
}
if (ds == null) return; //already freed
timer = new Timer();
timeout = System.currentTimeMillis() + (secs-1) * 1000;
timer.schedule(new AllocTask(this), secs * 1000);
}
}
void free() {
log("object released");
synchronized(lock) {
if (timer != null) {
timer.cancel();
timer = null;
}
allocs.remove(id);
if (worker != null) {
worker.active = false;
worker = null;
}
if (ds != null) {
ds.close();
ds = null;
}
}
}
void log(String msg) {
JFLog.log(id + ":" + msg);
}
}
private HashMap<String, Alloc> allocs = new HashMap<String, Alloc>();
private Object allocsLock = new Object();
private void relayTurn(String ip, int port, byte data[], ByteBuffer bb) throws Exception {
String id = ip + ":" + port;
Alloc alloc = getAlloc(id);
if (alloc == null) {JFLog.log("Unknown client:" + id); return;}
short channel = bb.getShort(0);
short length = bb.getShort(2);
if (channel != alloc.channel) {alloc.log("relayTurn:channel mismatch"); return;}
DatagramPacket dp = new DatagramPacket(data, 4, length);
dp.setAddress(alloc.relayaddr);
dp.setPort(alloc.relayport);
// alloc.log("relayTurn to:" + alloc.relayaddr.getHostAddress() + ":" + alloc.relayport);
alloc.ds.send(dp);
}
private Alloc getAlloc(String ip, int port, InetAddress addr) {
String id = ip + ":" + port;
Alloc alloc;
synchronized(allocsLock) {
alloc = allocs.get(id);
if (alloc != null) return alloc;
alloc = new Alloc();
alloc.ip = ip;
alloc.port = port;
alloc.id = id;
alloc.addr = addr;
allocs.put(id, alloc);
}
return alloc;
}
private Alloc getAlloc(String id) {
Alloc alloc;
synchronized(allocsLock) {
alloc = allocs.get(id);
}
return alloc;
}
private char chars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8' ,'9', 'a', 'b', 'c', 'd', 'e', 'f'};
private String nonceRandom() {
Random r = new Random();
StringBuilder sb = new StringBuilder();
for(int a=0;a<20;a++) {
sb.append(chars[r.nextInt(16)]);
}
return sb.toString();
}
private int min = 10000, max = 20000, next = 10000;
public void setPortRange(int min, int max) {
this.min = min;
this.max = max;
this.next = min;
}
private synchronized int getNextPort() {
int port = next++;
next++; //odd port to reserve
if (next >= max) next = min;
return port;
}
private boolean isClassicStun(long id1) {
return ((id1 >>> 32) != 0x2112a442);
}
private HashMap<String, String> tokens = new HashMap<String, String>();
private int nextToken = 0;
private synchronized String allocToken(String id) {
String token = String.format("%08x", nextToken++); //token MUST be 8 bytes
if (nextToken < 0) nextToken = 0;
tokens.put(token, id);
return token;
}
private String getTokenID(String token) {
String id = tokens.get(token);
tokens.remove(token);
return id;
}
private class Worker extends Thread {
public void run() {
DatagramPacket dp;
byte data[] = new byte[1500];
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(ByteOrder.BIG_ENDIAN);
int change_request_flgs;
String realm, nonce;
int lifetime;
InetAddress remoteAddr, localAddr;
String remoteip, localip;
int remoteip_int, remotePort;
int localip_int, localPort;
boolean auth, evenPort, username;
String resToken;
String f[];
Alloc alloc;
int relayip, relayport, channel;
InetAddress relayaddr;
byte ip4[] = new byte[4];
if (publicip != null) {
localip = publicip;
} else {
localAddr = ds.getInetAddress();
if (localAddr != null) {
localip = localAddr.getHostAddress();
if (localip.indexOf(":") != -1) localip = "127.0.0.1"; //IP6
} else {
localip = "127.0.0.1";
}
}
f = localip.split("[.]");
localip_int = 0;
for(int a=0;a<4;a++) {
localip_int <<= 8;
localip_int += JF.atoi(f[a]);
}
localPort = ds.getPort();
while (active) {
try {
dp = new DatagramPacket(data, 1500);
ds.receive(dp);
remoteAddr = dp.getAddress();
remoteip = remoteAddr.getHostAddress();
if (remoteip.indexOf(":") != -1) continue; //IP6
f= remoteip.split("[.]");
remoteip_int = 0;
for(int a=0;a<4;a++) {
remoteip_int <<= 8;
remoteip_int += JF.atoi(f[a]);
}
remotePort = dp.getPort();
int packetLength = dp.getLength();
//reset all values
change_request_flgs = 0;
realm = "";
nonce = "";
lifetime = defaultLifeTime;
auth = false;
alloc = null;
resToken = null;
evenPort = false;
relayip = -1;
relayport = -1;
relayaddr = null;
username = false;
channel = -1;
//decode response
int offset = 0;
short code = bb.getShort(0);
if (code >= 0x4000 && code <= 0x7fff) {
//it's TURN data
relayTurn(remoteip, remotePort, data, bb);
continue;
}
if (code == DATA_INDICATION) {
continue;
}
offset += 2;
int lengthOffset = offset;
short length = bb.getShort(offset);
if (length + 20 != packetLength) {
throw new Exception("STUN:bad packet:incorrect length");
}
offset += 2;
long id1 = bb.getLong(offset);
offset += 8;
long id2 = bb.getLong(offset);
offset += 8;
while (offset < packetLength) {
short attr = bb.getShort(offset);
offset += 2;
length = bb.getShort(offset);
offset += 2;
switch (attr) {
case USERNAME:
if (user == null) break;
username = (new String(data, offset, length).equals(user));
break;
case REALM:
realm = new String(data, offset, length);
break;
case NONCE:
nonce = new String(data, offset, length);
break;
case LIFETIME:
lifetime = bb.getInt(offset);
break;
case CHANGE_REQUEST:
change_request_flgs = bb.getInt(offset);
break;
case EVEN_PORT:
evenPort = true;
break;
case CHANNEL_NUMBER:
channel = bb.getShort(offset); //0x4000 - 0x7fff
if (channel < 0x4000) channel = -1;
break;
case RESERVATION_TOKEN:
resToken = new String(Arrays.copyOfRange(data, offset, offset + length));
break;
case MESSAGE_INTEGRITY:
alloc = getAlloc(remoteip, remotePort, remoteAddr);
if (nonce == null) {alloc.log("!nonce"); break;}
if (realm == null) {alloc.log("!realm"); break;}
if (alloc.nonce == null) {alloc.log("!alloc.nonce"); break;}
if (!nonce.equals(alloc.nonce)) {alloc.log("nonce mismatch"); break;}
if (!username) {alloc.log("!username"); break;}
bb.putShort(lengthOffset, (short)(offset)); //patch length
byte correct[] = calcMsgIntegrity(data, offset - 4);
byte supplied[] = Arrays.copyOfRange(data, offset, offset+20);
auth = Arrays.equals(correct, supplied);
// logKey(correct);
// logKey(supplied);
break;
case XOR_PEER_ADDRESS:
relayport = (bb.getShort(offset + 2) ^ bb.getShort(4)) & 0xffff;
relayip = (bb.getInt(offset + 4) ^ bb.getInt(4));
ip4[0] = (byte)((relayip & 0xff000000) >>> 24);
ip4[1] = (byte)((relayip & 0xff0000) >> 16);
ip4[2] = (byte)((relayip & 0xff00) >> 8);
ip4[3] = (byte)(relayip & 0xff);
relayaddr = InetAddress.getByAddress(ip4);
break;
}
offset += length;
if ((length & 0x3) > 0) {
offset += 4 - (length & 0x3); //padding
}
}
switch (code) {
case BINDING_REQUEST:
JFLog.log(remoteip + ":" + remotePort + ":BINDING_REQUEST");
offset = 0;
bb.putShort(offset, BINDING_RESPONSE);
offset += 2;
bb.putShort(offset, (short)12); //length
offset += 2;
bb.putLong(offset, id1);
offset += 8;
bb.putLong(offset, id2);
offset += 8;
if (isClassicStun(id1)) {
bb.putShort(offset, MAPPED_ADDRESS);
offset += 2;
bb.putShort(offset, (short)8); //length of attr
offset += 2;
bb.put(offset, (byte)0); //reserved
offset++;
bb.put(offset, (byte)1); //IP family
offset++;
bb.putShort(offset, (short)remotePort);
offset += 2;
bb.putInt(offset, remoteip_int);
offset += 4;
} else {
//use XOR_MAPPED_ADDRESS instead
bb.putShort(offset, XOR_MAPPED_ADDRESS);
offset += 2;
bb.putShort(offset, (short)8); //length of attr
offset += 2;
bb.put(offset, (byte)0); //reserved
offset++;
bb.put(offset, (byte)1); //IP family
offset++;
bb.putShort(offset, (short)(remotePort ^ bb.getShort(4)));
offset += 2;
bb.putInt(offset, remoteip_int ^ bb.getInt(4));
offset += 4;
}
dp = new DatagramPacket(data, offset);
dp.setAddress(remoteAddr);
dp.setPort(remotePort);
if ((change_request_flgs & 0x02) == 0x02) {
//different port
ds2.send(dp);
} else {
//same port
ds.send(dp);
}
break;
case ALLOCATE_REQUEST:
alloc = getAlloc(remoteip, remotePort, remoteAddr);
alloc.log("ALLOCATE_REQUEST");
if (!auth) {
sendError(alloc, ALLOCATE_FAILED, id1, id2, remoteAddr, remotePort, data, bb);
} else {
if (resToken != null) {
if (evenPort) throw new Exception("EVEN_PORT with RESERVATION_TOKEN");
Alloc evenAlloc = getAlloc(getTokenID(resToken));
if (evenAlloc == null) throw new Exception("Reservation not found");
if (!evenAlloc.evenPort) throw new Exception("Odd Port was not reserved");
alloc.ds = new DatagramSocket(evenAlloc.ds.getLocalPort() + 1);
} else {
alloc.ds = new DatagramSocket(getNextPort());
}
alloc.evenPort = evenPort;
offset = 0;
bb.putShort(offset, ALLOCATE_RESPONSE);
offset += 2;
lengthOffset = offset;
bb.putShort(offset, (short)0); //length (patch)
offset += 2;
bb.putLong(offset, id1);
offset += 8;
bb.putLong(offset, id2);
offset += 8;
if (evenPort) {
String token = allocToken(remoteip + ":" + remotePort);
bb.putShort(offset, RESERVATION_TOKEN);
offset += 2;
bb.putShort(offset, (short)token.length()); //length of attr
offset += 2;
System.arraycopy(token.getBytes(), 0, data, offset, token.length());
offset += token.length();
}
bb.putShort(offset, XOR_RELAY_ADDRESS);
offset += 2;
bb.putShort(offset, (short)8); //length of attr
offset += 2;
bb.put(offset, (byte)0); //reserved
offset++;
bb.put(offset, (byte)1); //IP family
offset++;
bb.putShort(offset, (short)(alloc.ds.getLocalPort() ^ bb.getShort(4)));
offset += 2;
bb.putInt(offset, localip_int ^ bb.getInt(4));
offset += 4;
bb.putShort(lengthOffset, (short)(offset - 20)); //length (patch)
dp = new DatagramPacket(data, offset);
dp.setAddress(remoteAddr);
dp.setPort(remotePort);
ds.send(dp);
alloc.setTimeout(defaultLifeTime);
}
break;
case REFRESH_REQUEST:
alloc = getAlloc(remoteip, remotePort, remoteAddr);
alloc.log("REFRESH_REQUEST");
if (!auth) {
sendError(alloc, REFRESH_FAILED, id1, id2, remoteAddr, remotePort, data, bb);
} else {
if (lifetime != 0) {
alloc.setTimeout(defaultLifeTime);
}
}
offset = 0;
bb.putShort(offset, REFRESH_RESPONSE);
offset += 2;
bb.putShort(offset, (short)8); //length
offset += 2;
bb.putLong(offset, id1);
offset += 8;
bb.putLong(offset, id2);
offset += 8;
bb.putShort(offset, LIFETIME);
offset += 2;
bb.putShort(offset, (short)4); //length of attr
offset += 2;
bb.putInt(offset, lifetime);
offset += 4;
dp = new DatagramPacket(data, offset);
dp.setAddress(remoteAddr);
dp.setPort(remotePort);
ds.send(dp);
if (lifetime == 0) alloc.free();
break;
case BIND_REQUEST:
alloc = getAlloc(remoteip, remotePort, remoteAddr);
alloc.log("BIND_REQUEST");
if (!auth || channel == -1) {
sendError(alloc, BIND_FAILED, id1, id2, remoteAddr, remotePort, data, bb);
} else {
synchronized(alloc.lock) {
alloc.setTimeout(defaultLifeTime); //refresh all
alloc.relayip = relayip;
alloc.relayaddr = relayaddr;
alloc.relayport = relayport;
alloc.channel = (short)channel;
if (alloc.worker == null) {
alloc.worker = new RelayWorker(alloc);
alloc.worker.start();
}
}
}
offset = 0;
bb.putShort(offset, BIND_RESPONSE);
offset += 2;
bb.putShort(offset, (short)8); //length
offset += 2;
bb.putLong(offset, id1);
offset += 8;
bb.putLong(offset, id2);
offset += 8;
bb.putShort(offset, LIFETIME);
offset += 2;
bb.putShort(offset, (short)4); //length of attr
offset += 2;
bb.putInt(offset, defaultLifeTime);
offset += 4;
dp = new DatagramPacket(data, offset);
dp.setAddress(remoteAddr);
dp.setPort(remotePort);
ds.send(dp);
break;
default:
JFLog.log(remoteip + ":" + remotePort + ":Unknown request:" + code);
break;
}
} catch (Exception e) {
if (active) JFLog.log(e);
}
}
done = true;
}
}
private void logKey(byte key[]) {
StringBuilder log = new StringBuilder();
for(int a=0;a<key.length;a++) {
int b = ((int)key[a]) & 0xff;
if (b < 0x10) log.append("0");
log.append(Integer.toString(b, 16));
}
JFLog.log("key=" + log);
}
private class AllocTask extends TimerTask {
Alloc alloc;
public AllocTask(Alloc alloc) {
this.alloc = alloc;
}
public void run() {
long now = System.currentTimeMillis();
synchronized(alloc.lock) {
if (alloc.timeout < now) {
alloc.log("object expired");
alloc.free();
}
}
}
}
private void sendError(Alloc alloc, short code, long id1, long id2, InetAddress remoteAddr, int remotePort, byte data[], ByteBuffer bb) throws Exception {
int offset, lengthOffset;
alloc.nonce = nonceRandom();
offset = 0;
bb.putShort(offset, code);
offset += 2;
lengthOffset = offset;
bb.putShort(offset, (short)0); //length (patch)
offset += 2;
bb.putLong(offset, id1);
offset += 8;
bb.putLong(offset, id2);
offset += 8;
bb.putShort(offset, ERROR_CODE);
offset += 2;
bb.putShort(offset, (short)4); //length of attr
offset += 2;
bb.putInt(offset, 0x401);
offset += 4;
bb.putShort(offset, REALM);
offset += 2;
bb.putShort(offset, (short)realm.length());
offset += 2;
System.arraycopy(realm.getBytes(), 0, data, offset, realm.length());
offset += realm.length();
if ((offset & 0x3) > 0) {
offset += 4 - (offset & 0x3); //padding
}
bb.putShort(offset, NONCE);
offset += 2;
bb.putShort(offset, (short)alloc.nonce.length());
offset += 2;
System.arraycopy(alloc.nonce.getBytes(), 0, data, offset, alloc.nonce.length());
offset += alloc.nonce.length();
bb.putShort(lengthOffset, (short)(offset - 20)); //length (patch)
DatagramPacket dp = new DatagramPacket(data, offset);
dp.setAddress(remoteAddr);
dp.setPort(remotePort);
ds.send(dp);
}
private class RelayWorker extends Thread {
Alloc alloc;
public volatile boolean active = true;
private static final int mtu = 1500 - 20 - 8; //20=IP 8=UDP
public RelayWorker(Alloc alloc) {
this.alloc = alloc;
}
public void run() {
//read packets from relay socket and send to owner
byte data[] = new byte[mtu + 4];
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(ByteOrder.BIG_ENDIAN);
bb.putShort(0, alloc.channel);
// alloc.log("Starting Worker:SendTo:" + alloc.addr.getHostAddress() + ":" + alloc.port + ",From:" + alloc.ds.getLocalPort() + ",channel=" + Integer.toString(alloc.channel, 16));
while (active) {
try {
DatagramPacket indp = new DatagramPacket(data, 4, mtu);
alloc.ds.receive(indp);
int len = indp.getLength();
bb.putShort(2, (short)len);
DatagramPacket outdp = new DatagramPacket(data, 0, len + 4);
outdp.setAddress(alloc.addr);
outdp.setPort(alloc.port);
ds.send(outdp);
} catch (Exception e) {
if (active) JFLog.log(e);
}
}
}
}
private static JBusServer busServer;
private JBusClient busClient;
private String config;
public class JBusMethods {
public void getConfig(String pack) {
busClient.call(pack, "getConfig", busClient.quote(busClient.encodeString(config)));
}
public void setConfig(String cfg) {
//write new file
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(JBusClient.decodeString(cfg).getBytes());
fos.close();
} catch (Exception e) {
JFLog.log(e);
}
}
public void restart() {
stun.close();
stun = new STUN();
stun.start();
}
}
public static int getBusPort() {
if (JF.isWindows()) {
return 33006;
} else {
return 777;
}
}
public static void main(String args[]) {
serviceStart(args);
}
//Win32 Service
private static STUN stun;
public static void serviceStart(String args[]) {
if (JF.isWindows()) {
busServer = new JBusServer(getBusPort());
busServer.start();
while (!busServer.ready) {
JF.sleep(10);
}
}
stun = new STUN();
stun.start();
}
public static void serviceStop() {
JFLog.log("Stopping service");
busServer.close();
stun.close();
}
}