Vanker
By:
Vanker

Fsockopen UDP connection problem

May 13, 2017 1.9k views
Networking PHP Apache Firewall Security Ubuntu 16.04

I am currently developing an APP that makes A UDP connection to another host to send commands (old quake3 engine game server) so you can moderate your gameserver without having to boot the game up on your pc.

I'm using laravel (overkill, but I'm learning the framework as well) and locally it works just fine, I can send the command and retrieve the message just as i want it to be.
Now when deploying, the app doesn't seem to work anymore, it doesn't send any commands and I can't seem to debug that, udp doesn't have status (sometimes shows it connected even though the host is dead) so I'm running out of ideas, the firewall is set-up to allow outgoing UDP/TCP connections(I've tried with the firewall disabled as well) to the port I'm sending commands to (29070) and when asking the support about any possible blocking they said no.
As you guys are probably way more experienced than me, is this a PHP problem or a VPS one?

$this->fp = fsockopen($this->host, $this->port, $this->errno, $this->errstr, 5);
        socket_set_timeout($this->fp, 5);   

and if I do

if ($this->fp)
    echo 'sucess';

It echoes out sucess, but you can't really trust that when it comes to UDP connections.

How should I proceed?

Thanks.

ps used lemp and lamp on ubuntu 16.04

also I've looked around for another laravel host service and none of them accept outgoing and/or incoming UDP connection, which kill's my application, so if any of you have any ideas please share, i'm desperate.

7 Answers

@Vanker

Are you remembering to add udp:// in front of the server address?
http://php.net/manual/en/function.fsockopen.php#example-5028

  • yes, yes ofc, game only accepts udp and it works locally as I've said.

    • @Vanker So just to try to understand what you're doing.

      You're running Laravel on a droplet, but where is Quake game server hosted? One the same droplet, on a different droplet or somewhere else completely?

      • Sorry for the delay.

        No, The quake game server is hosted on amazon by another person (I have no control over that) the person responsible by the game server send me a screenshot of the forwarded ports and it works cause when I use php artisan serve on my machine I can see the data just fine.

        • @Vanker
          Okay, but why are you not using the same setup on the droplet as on your local machine?

          • My machine is running W10 and the only difference other than the O.S is the php version (which I looked up and it isn't suppose to have any negative effect)

            My machine = PHP 5.6

            VPS UBUNTU 16.04 LAMP(tried with LEMP too) = php 7.0

@Vanker

It's really dependent on what you're passing as $host. I'll use 111.222.33.44 as our fake IP.

Instead of using something such as:

$this->fp = fsockopen('111.222.33.44', 29070, $this->errno, $this->errstr, 5);

... then it most likely will fail or cause problems. Instead, I'd recommend prefixing the IP with udp.

$this->fp = fsockopen('udp://111.222.33.44', 29070, $this->errno, $this->errstr, 5);

I created a very basic function to test fsockopen, this is what I used.

function udpStatus( $host, $port )
{
    $fp = fsockopen( "udp://" . $host, $port, $errNum, $errStr, 1.0 );

    if ( ! $fp )
    {
        return false;
    }
    else
    {
        fclose( $fp );

        return true;
    }
}

In the function, you can see that $host is prefixed with udp://. We can then run a conditional check using:

if ( udpStatus( 'HOST_OR_IP', 29070 ) )
{
    echo 'Connection Status: Success';
}
else
{
    echo 'Connection Status: Failed';
}

Replace HOST_OR_IP with the host or IP you're connecting to.

With the udp:// prefix, the result from one Droplet connecting to another is Success. If we remove the prefix from the function, it fails.

  • @Vanker

    I've tested the above on two Droplets in NYC 3 from both the CLI and Web and the result is the same -- whether running the above as php udp.php from the CLI or over PHP's build in web server via the web.

    Though, as expected, with the firewall blocking all ports except 22, 80, and 443, when trying to connect to 29070 via UDP, the result is still successful -- which is the nature of UDP. There's not a guarantee that the message will be delivered.

    Note: I named the file udp.php just for testing, hence the reference in the command.

@Vanker

Doing a little more digging :-). On the same two NYC 3 Droplets, I setup one droplet as a sender and the other as a receiver for UDP.

One the first droplet, I created a file called udp.php and within it, I used the core example that PHP provides for socket_create.

<?php
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);

$msg = "Ping!";
$len = strlen($msg);

socket_sendto($sock, $msg, $len, 0, 'REMOTE_IP', 29070);
socket_close($sock);

Where REMOTE_IP is the IP of the second Droplet that is supposed to receive the message. All the above is going to do is send a message "Ping!".

On the second Droplet, I created a PHP file called receiver.php and within it, I placed:

<?php
//Create socket.
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if (!$socket) { die("socket_create failed.\n"); }

//Set socket options.
socket_set_nonblock($socket);
socket_set_option($socket, SOL_SOCKET, SO_BROADCAST, 1);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
if (defined('SO_REUSEPORT'))
    socket_set_option($socket, SOL_SOCKET, SO_REUSEPORT, 1);

//Bind to any address & port 29070.
if(!socket_bind($socket, '0.0.0.0', 29070))
    die("socket_bind failed.\n");

//Wait for data.
$read = array($socket); $write = NULL; $except = NULL;
while(socket_select($read, $write, $except, NULL)) {

    //Read received packets with a maximum size of 5120 bytes.
    while(is_string($data = socket_read($socket, 5120))) {
        echo $data;
    }

}

That snippet came from here

On the second droplet, to start the receiver, all I did was run php receiver.php from the command line. Since it's within a while loop, it'll keep running until I exit the script.

I then went to the other droplet and ran php udp.php and "Ping!" shows up on the other end, as was expected.

So you may want to look in to the Sockets Library instead of using fsockopen in this particular case.

http://php.net/manual/en/book.sockets.php

  • Very nice example I did a test similar like yours yersteday with NETCAT

    I set-up a NC listener on one droplet and sent "test" message from another droplet(I even did from my local machine and it worked).

    Even when I opened the ports on UFW I couldn't receive the message, only when I disabled both firewalls I could get that to work, but even then my script wouldn't work.

    As for the socket_create I've tried that as well, as I cannot use socket_set_timeout for that, the page just keeps trying to establish a connection forever.

    And yes I'm specifying the udp://before, if you don't it tries default type (which is TCP).

    One could think that the problem lies on the Amazon server (game host) but why would it work on my local machine and not here?

@Vanker

In the second example, you shouldn't need to set a timeout -- socket_sendto is either going to send the message and return the number of bytes sent, or return false. The issue is, with UDP, you're not going to see false returned since it's one-way -- so it'll send the message or nothing at all.

I tested the second example with socket_sendto with ufw active on both. It successfully sent the message when port 29070 was open on the receiving server and, as expected, sent nothing if the port was blocked. Back to the nature of UDP, even with it blocked and the mindset we should see a failure, it doesn't fail so to speak, just nothing is echo'ed or received.

...

As for ufw, what I would recommend is starting fresh. You may have something that is blocking the connection in the rules, even if you don't see it in the status listing.

So let's try this first.

1). Disable ufw

ufw disable

2). Reset ufw

ufw reset

3). Setup default rules. We'll deny incoming, allow outgoing.

ufw default deny incoming
ufw default allow outgoing

At this point, if you enabled ufw, you'd be locked out as no incoming connections are being allowed at this point. We need to fix that.

First, let's make sure SSH is allowed through.

ufw allow 22/tcp

Now we need to add any other ports to the rule set that you need to listen on, that means, any ports you need to accept connections on.

For the server that I was sending the UDP message from, only SSH is open. I enabled ufw after that first allow.

One the second server, I allowed SSH using the above, and then I opened port 29070 by specifying udp:

ufw allow 29070/udp

That means only udp connections are allowed on 29070 -- TCP won't work.

I prefer to be explicit when opening ports, which is why I use port/protocol -- ufw allows other ways, but they don't always work as expected. When I use this method, I know it works.

Once you've done all the above, enable ufw

ufw enable

@Vanker

As another test, we could use something like this to bounce between two servers.

client.php

<?php
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);

$msg = "Ping!" . PHP_EOL;
$len = strlen($msg);

if ( ! socket_sendto( $sock, $msg, $len, 0, 'REMOTE_IP', 29070 ) )
{
    $errorcode = socket_last_error();
    $errormsg  = socket_strerror( $errorcode );

    die( "Could not send data: [$errorcode] $errormsg" . PHP_EOL );
}

if ( socket_recv( $sock , $reply , 2045 , MSG_WAITALL ) === FALSE )
{
    $errorcode = socket_last_error();
    $errormsg  = socket_strerror( $errorcode );

    die( "Could not receive data: [$errorcode] $errormsg" . PHP_EOL );
}

echo "Reply: $reply";

socket_close($sock);

server.php

<?php
/**
 * Create UDP Socket
 */
if( ! ( $sock = socket_create( AF_INET, SOCK_DGRAM, 0 ) ) )
{
    $errorcode = socket_last_error();
    $errormsg  = socket_strerror( $errorcode );

    die( "Couldn't create socket: [$errorcode] $errormsg" . PHP_EOL );
}

echo 'Socket created' . PHP_EOL;

/**
 * Bind to Source Address
 */
if( ! socket_bind( $sock, '0.0.0.0' , 29070 ) )
{
    $errorcode = socket_last_error();
    $errormsg  = socket_strerror( $errorcode );

    die( "Could not bind socket : [$errorcode] $errormsg" . PHP_EOL );
}

echo 'Socket Bind: OK' . PHP_EOL;

while(1)
{
    echo "Waiting for data ..." . PHP_EOL;

    /**
     * Receive Data
     */
    $r = socket_recvfrom( $sock, $buf, 512, 0, $remote_ip, $remote_port );

    echo "$remote_ip : $remote_port -- " . $buf;

    /**
     * Send Data Back to Client
     */
    socket_sendto( $sock, "OK " . $buf , 100 , 0 , $remote_ip , $remote_port );
}

socket_close( $sock );

You'll want to change REMOTE_IP on line 7 of client.php to match the IP you're trying to connect to.

On the remote server, we would run php server.php. Then on the client, we'll run php client.php.

client.php sends Ping! and server.php responds with OK Ping! which shows on the client server.

...

This is confirmation of sorts when it comes to whether the message was actually sent. Now, in this example, if the server you're trying to send to has the port blocked, it will hang, but there's way around that as well.

We can use socket_set_option, like so:

socket_set_option( $sock, SOL_SOCKET, SO_RCVTIMEO, [ "sec" => 10, "usec" => 0 ] );

You'd change sec from 10 to however many seconds you want to allow the connection attempt to run. So our new client.php script would look like:

<?php
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);

socket_set_option( $sock, SOL_SOCKET, SO_RCVTIMEO, [ "sec" => 10, "usec" => 0 ] );

$msg = "Ping!" . PHP_EOL;
$len = strlen($msg);

if ( ! socket_sendto( $sock, $msg, $len, 0, 'REMOTE_IP', 29070 ) )
{
    $errorcode = socket_last_error();
    $errormsg  = socket_strerror( $errorcode );

    die( "Could not send data: [$errorcode] $errormsg" . PHP_EOL );
}

if ( socket_recv( $sock , $reply , 2045 , MSG_WAITALL ) === FALSE )
{
    $errorcode = socket_last_error();
    $errormsg  = socket_strerror( $errorcode );

    die( "Could not receive data: [$errorcode] $errormsg" . PHP_EOL );
}

echo "Reply: $reply";

socket_close($sock);

In the above, if it doesn't connect and send in 10 seconds, you'll receive something like:

Could not receive data: [11] Resource temporarily unavailable
  • @Vanker

    For both examples, the only thing running on my 2x Droplets is PHP 7.0.

    I've tested the above locally on my MacBook Pro using PHP 7.1 as well. The results are the same. So if the connection is getting blocked, it's most likely due to the firewall.

    By default, a base image of Ubuntu doesn't have any firewall rules active that would block, so if there's a block, it's most likely due to the firewall or some lingering rule(s).

    • @jtittle
      So my code is like this now.

      Route::get('sock', function (){
      
          $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
      
          $msg = str_repeat(chr(255), 4).'rcon '.'passwordforremotecontrol'.' '. 'status';
          $len = strlen($msg);
      
          if ( ! socket_sendto( $sock, $msg, $len, 0, '111.111.111.111', 29070 ) )
          {
              $errorcode = socket_last_error();
              $errormsg  = socket_strerror( $errorcode );
      
              die( "Could not send data: [$errorcode] $errormsg" . PHP_EOL );
          }
      
          if ( socket_recv( $sock , $reply , 5000 , MSG_WAITALL ) === FALSE )
          {
              $errorcode = socket_last_error();
              $errormsg  = socket_strerror( $errorcode );
      
              die( "Could not receive data: [$errorcode] $errormsg" . PHP_EOL );
          }
      
          echo "Reply: $reply";
      
          socket_close($sock);
      }
      

      And like I've said, It just stands there, trying to connect, no message no error, nothing.

      I restarted the server now, will add the

      socket_set_option( $sock, SOL_SOCKET, SO_RCVTIMEO, [ "sec" => 10, "usec" => 0 ] );
      

      part

    • @jtittle As you said, after trying to connect after 10 seconds it just returns

      Could not receive data: [11] Resource temporarily unavailable.

      Did the firewall rules like you said.

      Any other ideas?

      • @Vanker

        Larvel should simply execute the closure as it's defined, though so we can eliminate it from being the issue, have you tested the code as I provided it on its own? As in just creating two PHP files client.php and server.php, place them on separate servers, and then tried to execute?

        If not, I'd try that. It doesn't matter where you stick them. I used /usr/local/src.

        While I wouldn't think Laravel would be the issue, I'm not overly familiar with it as I prefer to roll my own when it comes to code -- Laravel is nice, probably the nicest of most frameworks I've seen, but it's bulky and tries to do everything (like most do).

        I'd just copy and paste the code in to the files (and change that one line).

        ...

        I ran a test wrapping the code in a closure and it still worked, though I'd like to isolate things down to just barebones PHP.

        ...

        If it still doesn't work, and the ports you're allowing are correct, I'm a bit stumped.

        If you would, run:

        ufw status numbered
        

        On both servers and post the output to a code block.

        • I mean I tried that with the connecting netcat between two droplets and it worked, isn't the same thing?

          I already managed to communicate between droplets, so we know that is not an issue.

          Also the code works on my machine, which says 2 things, first the game host (amazon) is not declining/refusing/blocking the 29070 udp port and secondly the code works, which leads me to thinking that there is something wrong with the vps and/or some networking issue on this end.

          • @Vanker

            Generally, when there's an issue with code connecting to a port, I'll use code to test the issue, which is why I made that recommendation. I expect tools such as nc to work as expected, though when it comes to code, I've learned to expect the unexpected.

            I've ran in to a number of cases where I expected a framework to do one thing, but it didn't -- though when trying to do the same thing with just plain code, it worked as expected. That could be the case here, but it also may result in the same -- that's why we test.

            ...

            As for the Droplet (VPS), DigitalOcean uses minimal images last I recall, so the only thing that gets setup is a base image -- from there, however you configure or don't configure it is what you're left with.

            If they aren't blocking UDP at the network level, then it's either the firewall or there's something really odd happening on the VPS which isn't normal. I've never ran in to an issue where, when trying to connect from one to another, the connection was blocked or failed unless:

            1). There was a network issue that prevented it;
            2). The firewall was blocking it on one or both ends;
            3). The code wasn't right, thus the connection never would have worked.

  • [deleted]

@jtittle For some reason I couldn't reply to your last comment.

I got your point now, will test with the code you specified although I already destroyed the second droplet (was just testing anyways) so it will take some time.

Let's say it work the way we expect it to, what could be the case then?

Is there any chance that the game host (amazon) could block certain ips or range of ips or anything like that?

It's been a week now since I'm trying to figure this problem out and even my hair is turning gray already.

Also, I'd try to deploy on another hosting service to check if it works there, I'm just a programmer so I don't know a lot about setting up a server from ground, so I MAY have fucked up somewhere (read tutorials on how to set-up) but every host that I found does not allow incoming and/or outgoing udp connections.

  • @Vanker

    It wouldn't be the first time a provider has blocked an IP range, or even a single IP. You could get in touch with AWS support, provide them with the IP, and see what they say. That'd at least allow you to rule them out.

    What I would do, ideally, is run client.php on a Droplet and server.php on an AWS instance. If it is an issue somewhere between DigitalOcean and AWS, you won't be able to tell if you're only testing between two Droplets. If, when you run client.php, it responds as expected and you do receive the message from server.php on the AWS instance, then that narrows things down a lot.

    If the UDP connection fails from DigitalOcean => AWS (Game Server) but doesn't fail between the new Droplet and a new AWS instance, then the only conclusion I can think of would be an issue with the game server itself.

    • @jtittle
      Errm, when trying to run php server.php with the exact same code you entered it says:

      root@lamp-512mb-nyc2-01:/var/www/html/test# php server.php
      Socket created
      PHP Warning:  socket_bind(): unable to bind address [98]: Address already in use in /var/www/html/test/server.php on line 18
      Could not bind socket : [98] Address already in use
      

      Which I remember seeing something about this at some point and ignoring

      • @Vanker

        The Address already in use message means that it couldn't bind to the IP as it's being used by another service.

        If Apache, NGINX, a game server, or another web server or service is binding to 0.0.0.0 or the IP specified, you'd need to stop that service before running php server.php, else it will fail.

        In your case, it'd be Apache, so we'd need to run:

        service apache2 stop
        
        • Yep, it worked, pinged just fine.

          Unfortunately I can't put the server file on the AWS as I have no control over that.

          Now even in my local environment the script started to behave just like the remote, no answer from the game server go figure...

@Vanker

I would deploy a basic AWS instance for testing purposes. I wouldn't use that script on a production game server since you'd need to install PHP on the server to run it.

On the AWS test instance, you'd run the server.php script and test the connection to it as well as it's response from a DigitalOcean Droplet that has the client.php file. That way you're testing across both networks.

Without setting up a test instance on AWS and testing the connection between DigitalOcean and the AWS instance, the only conclusion I can come to is that the issue is on the game server. The cause is hard to determine since I don't know anything about the game server or how it was setup.

  • Yeah, well, unfortunately that's not possible for me.

    Thanks for the help and for the explanations, you show real knowledge of what you are talking about.

Have another answer? Share your knowledge.