javasocketsudphole-punching

udp hole punch wrong ports?


Question: Are the ports wrong (to make an udp punch)?

I got a Java server that save ip and ports from connecting clients and servers. The ip and ports is sent to the client and server so they can start a udp hole punch. I use 2 computer connected to the same NAT. But it refuse to work.

The java server log say:

Send server global ip and port 61721 to client
Send client global ip and port 63105 to server

The server and client start to send udp packets but after some time they timeout. I used Wireshark to check the hole punch and the client log looks like this:

Source          Destination     Info
192.168.1.78    206.217.173.176 UDP source port: 50701 Destination Port: 61721
192.168.1.78    206.217.173.176 UDP source port: 50701 Destination Port: 61721
... + 50 more   

Server log:

Source          Destination     Info
192.168.1.73    206.217.173.176 UDP source port: 6510 Destination Port: 63105
192.168.1.73    206.217.173.176 UDP source port: 6510 Destination Port: 63105
... + 50 more   

No packets from the client is found on the server log. And no server packets is found on the client log.

This is the Java server code:

Full code can be found on: https://github.com/Parakoopa/GMnet-GATE-PUNCH/tree/master/src/main/java/org/parakoopa/gmnetgate/punch

Server.java

package org.parakoopa.gmnetgate.punch;

import com.google.gson.annotations.Expose;
import java.net.Socket;

public class Server {
     /**
     * Contains the IP of this server
     */
    @Expose private String ip = "";
     /**
     * Contains the Ports of the Server.
     * A port is assigned to an ip, so only one ip per server is possible.
     */
    private Integer port = 0;

    /**
     * Contains the TCP Sockets of the Server.
     * For sending the connection requests to the servers.
     */
    private Socket tcp_socket = null;
    /**
     * The 8 data strings.
     */
    @Expose private String data1 = "";
    @Expose private String data2 = "";
    @Expose private String data3 = "";
    @Expose private String data4 = "";
    @Expose private String data5 = "";
    @Expose private String data6 = "";
    @Expose private String data7 = "";
    @Expose private String data8 = "";

    /**
     * Time the server was created
     */
    @Expose private long createdTime;

    public Server(String ip) {
        this.createdTime = System.currentTimeMillis() / 1000L;
        this.ip = ip;
    }

     /**
     * Contains the Ports of the Server.
     * A port is assigned to an ip, so only one ip per server is possible.
     */
    public Integer getPort() {
        return port;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    /**
     * Contains the TCP Sockets of the Server.
     * For sending the connection requests to the servers.
     */
    public Socket getTCPsocket() {
        return tcp_socket;
    }

    public void setTCPsocket(Socket tcp_socket) {
        this.tcp_socket = tcp_socket;
    }

    public String getData1() {
        return data1;
    }

    public void setData1(String data1) {
        this.data1 = data1;
    }

    public String getData2() {
        return data2;
    }

    public void setData2(String data2) {
        this.data2 = data2;
    }

    public String getData3() {
        return data3;
    }

    public void setData3(String data3) {
        this.data3 = data3;
    }

    public String getData4() {
        return data4;
    }

    public void setData4(String data4) {
        this.data4 = data4;
    }

    public String getData5() {
        return data5;
    }

    public void setData5(String data5) {
        this.data5 = data5;
    }

    public String getData6() {
        return data6;
    }

    public void setData6(String data6) {
        this.data6 = data6;
    }

    public String getData7() {
        return data7;
    }

    public void setData7(String data7) {
        this.data7 = data7;
    }

    public String getData8() {
        return data8;
    }

    public void setData8(String data8) {
        this.data8 = data8;
    }

    public long getCreatedTime() {
        return createdTime;
    }

}

TCPConnection.java:

package org.parakoopa.gmnetgate.punch;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * Handles incoming TCP connections.
 * Stores server sockets and on client request send server ports to client and
 * client ports to server via the stored socket.
 * @author Parakoopa
 */
public class TCPConnection implements Runnable {

    private Mediator main;
    private Socket client;
    private ServerSocket server;
    /** Game Maker Studio seperates strings in buffers with this char (buffer_string). */
    private char gm_string_seperator = 0;
    /** True, if the "reg" command was used on this connection **/
    private boolean isServer = false;

    /**
     * Set's up a new connection listener that handles all packets of one connection.
     * @param main Mediator class instance that this server was created with.
     * @param client Socket that the client is connected to.
     * @param server Our TCP server socket the client is connected to. (not actually used)
     */
    public TCPConnection(Mediator main, Socket client, ServerSocket server) {
        this.server = server;
        this.client = client;
        this.main = main;
    }

    /**
     * Starts listening for incoming packets and responds to it.
     */
    @Override
    public void run() {
        String debug_string = this.client.getInetAddress().getHostAddress()+":"+this.client.getPort()+" | TCP | ";
        Mediator.log(debug_string+" Connected!",true);
        try {
            //TcpNoDelay configures the socket to transfer messages immediately, otherwise GM:S won't pick them up
            this.client.setTcpNoDelay(true);
            //Input and Output streams. We write bytes out and take Strings in.
            OutputStream out = client.getOutputStream();
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(client.getInputStream()));

            String inputLine;
            Server serverObj;
            //Process all packets. This while loop will stop when the peer disconnected.
            while ((inputLine = in.readLine()) != null) {
                //This will kill Threads (or commands) if the client don't send
                //all of the data expected. Needed otherwise this could lead
                //to many hanging threads.
                client.setSoTimeout(1000);
                //Cleans string, it might contain some garbage characters.
                inputLine = inputLine.replaceAll("\\p{C}", "");
                switch (inputLine) {
                    case "reg2":
                        Mediator.log(debug_string+" Server wants to register!",true);
                        //A server wants to register/reregister. We put the socket in the socket map so we can use it later.
                        //Check version compatibility
                        String version = in.readLine().replaceAll("\\p{C}", "");
                        Mediator.log(debug_string+" Version: "+version,true);
                        if (!(Mediator.versionCompare(version,Mediator.getUdphpMin()) >= 0)) {
                            //For now just silently end the connection.
                            //Proper error messages will follow in the next release
                            Mediator.log(debug_string+" Server not accepted. Version too old.",true);
                            client.close();
                            return;
                        }
                        Mediator.log(debug_string+" Server registered!",false);

                        serverObj = this.main.getServer(this.client.getInetAddress().getHostAddress());
                        this.isServer = true;
                        serverObj.setTCPsocket(this.client);
                        //Write the 8 data strings
                        serverObj.setData1(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 1: "+serverObj.getData1(),true);
                        serverObj.setData2(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 2: "+serverObj.getData2(),true);
                        serverObj.setData3(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 3: "+serverObj.getData3(),true);
                        serverObj.setData4(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 4: "+serverObj.getData4(),true);
                        serverObj.setData5(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 5: "+serverObj.getData5(),true);
                        serverObj.setData6(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 6: "+serverObj.getData6(),true);
                        serverObj.setData7(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 7: "+serverObj.getData7(),true);
                        serverObj.setData8(in.readLine().replaceAll("\\p{C}", ""));
                        Mediator.log(debug_string+" Data 8: "+serverObj.getData8(),true);
                    break;
                    case "connect":
                        //A client wants to connect. Now the interesting part begins
                        //Wait for next line that contains the requested IP adress.
                        String requested_server = in.readLine().replaceAll("\\p{C}", "");
                        String debug_string2 = debug_string + " Client <-> "+requested_server+" ->";
                        Mediator.log(debug_string2+" Connecting...",false);
                        if (this.main.getServerMap().containsKey(requested_server)) {
                            //SERVER FOUND
                            serverObj = this.main.getServer(requested_server);
                            //get server connection socket from the map (stored above)
                            Socket gameserver = serverObj.getTCPsocket();
                            if (!gameserver.isClosed()) {
                                String connect_to_server = requested_server;
                                //Get server port
                                int connect_to_port = serverObj.getPort();
                                //Send server port to client
                                Mediator.log(debug_string2+" Found server",true);
                                Mediator.log(debug_string2+" Send server port "+connect_to_port+" to client",true);
                                out.write((byte) 255);
                                out.write((connect_to_server+this.gm_string_seperator).getBytes());
                                out.write((String.valueOf(connect_to_port)+this.gm_string_seperator).getBytes());
                                //Send buffer to client
                                out.flush();
                                //Get client port
                                Client clientObj = this.main.getClient(this.client.getInetAddress().getHostAddress());
                                int connect_to_port_server = clientObj.getPort();
                                Mediator.log(debug_string2+" Send client port "+connect_to_port_server+" to server",true);
                                //Get an output stream for the server socket. We will contact the server with this.
                                OutputStream out_server = gameserver.getOutputStream();
                                out_server.write((byte) 255);
                                out_server.write((this.client.getInetAddress().getHostAddress()+this.gm_string_seperator).getBytes());
                                out_server.write(String.valueOf(connect_to_port_server+this.gm_string_seperator).getBytes());
                                //Send buffer to server
                                out_server.flush();
                                //We are done! Client and Server now connect to each other and the hole is punched!
                                Mediator.log(debug_string2+" CONNECTED!",false);
                            } else {
                                //SERVER FOUND BUT SOCKET IS DEAD
                                Mediator.log(debug_string+" CONNECTION FAILED - Server not reachable",false);
                                out.write((byte) 254);
                                out.flush();
                            }   
                        } else {
                            //SERVER NOT FOUND
                            Mediator.log(debug_string+" CONECTION FAILED - Server not found",false);
                            out.write((byte) 254);
                            out.flush();
                        }
                        this.main.destroyClient(this.client.getInetAddress().getHostAddress());
                    break;
                    case "lobby2":
                        if (Mediator.isLobby() || Mediator.isTesting()) {
                            Mediator.log(debug_string+" Sending lobby based on requested filters",true);

                            HashMap<String, Server> servers =  new HashMap<String, Server>(main.getServerMap());

                            String filter_data1 = in.readLine().replaceAll("\\p{C}", "");
                            String filter_data2 = in.readLine().replaceAll("\\p{C}", "");
                            String filter_data3 = in.readLine().replaceAll("\\p{C}", "");
                            String filter_data4 = in.readLine().replaceAll("\\p{C}", "");
                            String filter_data5 = in.readLine().replaceAll("\\p{C}", "");
                            String filter_data6 = in.readLine().replaceAll("\\p{C}", "");
                            String filter_data7 = in.readLine().replaceAll("\\p{C}", "");
                            String filter_data8 = in.readLine().replaceAll("\\p{C}", "");
                            final String filter_sortby = in.readLine().replaceAll("\\p{C}", "");
                            final String filter_sortby_dir = in.readLine().replaceAll("\\p{C}", "");
                            String filter_limit = in.readLine().replaceAll("\\p{C}", "");

                            //Skip servers with <INV> gamename (this might happen if a server was created using UDP connection but never initialized via TCP)
                            //TODO: Remove these invalid servers after some time.
                            Iterator<Map.Entry<String, Server>> iterInv = servers.entrySet().iterator();
                            while (iterInv.hasNext()) {
                                Map.Entry<String, Server> entry = iterInv.next();
                                if (entry.getValue().getData1().equals("<INV>")) {
                                    iterInv.remove();
                                }
                            }

                            if (!"".equals(filter_data1)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData1().equals(filter_data1)) {
                                        iter.remove();
                                    }
                                }
                            }

                            if (!"".equals(filter_data2)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData2().equals(filter_data2)) {
                                        iter.remove();
                                    }
                                }
                            }

                            if (!"".equals(filter_data3)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData3().equals(filter_data3)) {
                                        servers.remove(entry.getKey());
                                    }
                                }
                            }

                            if (!"".equals(filter_data4)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData4().equals(filter_data4)) {
                                        iter.remove();
                                    }
                                }
                            }

                            if (!"".equals(filter_data5)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData5().equals(filter_data5)) {
                                        iter.remove();
                                    }
                                }
                            }

                            if (!"".equals(filter_data6)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData6().equals(filter_data6)) {
                                        iter.remove();
                                    }
                                }
                            }

                            if (!"".equals(filter_data7)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData7().equals(filter_data7)) {
                                        iter.remove();
                                    }
                                }
                            }

                            if (!"".equals(filter_data8)) {
                                Iterator<Map.Entry<String, Server>> iter = servers.entrySet().iterator();
                                while (iter.hasNext()) {
                                    Map.Entry<String, Server> entry = iter.next();
                                    if (!entry.getValue().getData8().equals(filter_data8)) {
                                        iter.remove();
                                    }
                                }
                            }

                            Server[] arr = servers.values().toArray(new Server[servers.values().size()]);
                            Arrays.sort(arr, new Comparator<Server>() {
                                @Override
                                public int compare(Server o1, Server o2) {
                                    int mp = 1;
                                    int rt = 0;
                                    if ("ASC".equals(filter_sortby_dir)) {
                                        mp = -1;
                                    }
                                    switch (filter_sortby) {
                                        default:
                                        case "date":
                                            rt = new Long(o1.getCreatedTime()).compareTo(o2.getCreatedTime()) * mp;
                                        break;
                                        case "data1":
                                            rt = o1.getData1().compareTo(o2.getData1()) * mp;
                                        break;
                                        case "data2":
                                            rt = o1.getData2().compareTo(o2.getData2()) * mp;
                                        break;
                                        case "data3":
                                            rt = o1.getData3().compareTo(o2.getData3()) * mp;
                                        break;
                                        case "data4":
                                            rt = o1.getData4().compareTo(o2.getData4()) * mp;
                                        break;
                                        case "data5":
                                            rt = o1.getData5().compareTo(o2.getData5()) * mp;
                                        break;
                                        case "data6":
                                            rt = o1.getData6().compareTo(o2.getData6()) * mp;
                                        break;
                                        case "data7":
                                            rt = o1.getData7().compareTo(o2.getData7()) * mp;
                                        break;
                                        case "data8":
                                            rt = o1.getData8().compareTo(o2.getData8()) * mp;
                                        break;
                                    }
                                    return rt;
                                }
                            });

                            if (!"".equals(filter_limit) && Integer.valueOf(filter_limit) <= arr.length) {
                                arr = Arrays.copyOfRange(arr, 0, Integer.valueOf(filter_limit));
                            }

                            Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
                            String json = gson.toJson(arr);

                            out.write((byte) 249);
                            out.write(json.getBytes());
                            out.write(10);
                            //Send buffer to server
                            out.flush();
                        }
                    break;
                    case "istesting":
                        out.write((byte) 248);
                        if (Mediator.isTesting()) {
                            Mediator.log(debug_string+" Sending if testing is enabled",true);
                            out.write((byte) 1);
                        } else {
                            out.write((byte) 0);
                        }
                        //Send buffer
                        out.flush();
                    break;
                    case "testinginfos":
                        out.write((byte) 247);
                        if (Mediator.isTesting()) {
                            Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
                            out.write(Mediator.getName().getBytes());
                            out.write(10);
                            out.write(Mediator.getVersion().getBytes());
                            out.write(10);
                            out.write(Mediator.getUdphpMin().getBytes());
                            out.write(10);
                            Mediator.log(debug_string+" Sending testing information",true);
                        } else {
                            out.write((byte) 0);
                        }
                        //Send buffer
                        out.flush();
                    break;
                    case "version":
                        out.write((byte) 246);
                        out.write(Mediator.getVersion().getBytes());
                        out.write(10);
                        Mediator.log(debug_string+" Sending version information",true);
                        //Send buffer
                        out.flush();
                    break;
                    default:
                        //Ignore unknown commands (client disconnection will cause an unknown command)
                    break;
                }
                //Disable timout again and wait for next command
                client.setSoTimeout(0);
            }
            client.close();
            Mediator.log(debug_string+" Disconnected!",true);
            //Cleanup, when they loose TCP connection, this data can't be used anymore, so it's safe to remove
            if (this.isServer) {
                this.main.destroyServer(this.client.getInetAddress().getHostAddress());
                Mediator.log(debug_string+" Server deleted!",false);
            }
        } catch (Exception ex) {
            Mediator.log(debug_string+" Disconnected (e: "+ex.getClass().getName()+")",true);
            //Cleanup, when they loose TCP connection, this data can't be used anymore, so it's safe to remove
            if (this.isServer) {
                this.main.destroyServer(this.client.getInetAddress().getHostAddress());
                Mediator.log(debug_string+" Server deleted!",false);
            }
        }
    }
}

Solution

  • The ports where not wrong. The NAT had been updated by my provider behind my back. They made it more "secure". UDP Punch is not possible anymore with my NAT because it treat punch as an attack and block it.