Connecting to TCPServer from outside the LAN

I’ve got a photon set up as a TCPServer listening for connections on 10.87.0.180 port 90. This works fine if I’m on the same LAN as the photon and I connect to the photon via a browser. The photon responds with a web page and everything is great. However, if I forward port 90 to the photon and try to connect from the WAN side, I get some strange behavior. In chrome I get ERR_EMPTY_RESPONSE, and in Safari I get ‘Safari Cannot open the page because the network connection was lost.’

I have various other webservers running behind this router, ports forwarded the same way. This is unique to the photon. I have also tried other ports. Additionally, this site is connected to another site via an always-on site-to-site VPN and I get the same failure from the far end of the VPN when trying to connect to this photon, however, I have hundreds of servers on either end of this VPN all working fine, no matter which end of the VPN you connect to them from.

This seems to be a routing problem, as if the photon just doesn’t like the NAT.

Anyone have any success forwarding ports to a photon being used as a TCPServer or have any advice?

// This #include statement was automatically added by the Particle IDE.
#include <NCD2Relay.h>
NCD2Relay relayController;

// Our remote control buttons
int buttonPin1 = A0;
int buttonPin2 = A1;
int buttonPin3 = D2;
int buttonPin4 = D3;
int buttonPin5 = D4;
int buttonPin6 = D5;
int buttonPin7 = D6;
int buttonPin8 = D7;

TCPClient webClient;
TCPServer webServer = TCPServer(90);
char myIpAddress[24];
String readString;

long relayDuration = 0;
long relayStartTime = 0;

void setup() {
    //Turn off wifi so we can set up our static ip
    WiFi.off();
    //Set static ip info
    IPAddress myAddress(10,87,0,180);
    IPAddress netmask(255,255,252,0);
    IPAddress gateway(10,87,0,1);
    IPAddress dns(10,87,0,1);
    //Actually set the static ip
    WiFi.setStaticIP(myAddress, netmask, gateway, dns);
    //Tell the photon to use the static ip
    WiFi.useStaticIP();
    WiFi.on();
    WiFi.connect();
    //pins for RF remote inputs
    pinMode( A0 , INPUT_PULLUP); //remote pin 1
    pinMode( A1 , INPUT_PULLUP); //remote pin 2
    pinMode( D2 , INPUT_PULLUP); //remote pin 3
    pinMode( D3 , INPUT_PULLUP); //remote pin 4
    pinMode( D4 , INPUT_PULLUP); //remote pin 5
    pinMode( D5 , INPUT_PULLUP); //remote pin 6
    pinMode( D6 , INPUT_PULLUP); //remote pin 7
    pinMode( D7 , INPUT_PULLUP); //remote pin 8
    
    relayController.setAddress(1, 1, 1); // set the address for the relay board

    Particle.variable("ipAddress", myIpAddress, STRING); // make it so we can read the IP address variable of the photon in the particle console
    IPAddress myIp = WiFi.localIP(); // get the IP of the photon which is handed out via DHCP
    sprintf(myIpAddress, "%d.%d.%d.%d", myIp[0], myIp[1], myIp[2], myIp[3]); //Set the myIpAddress variable
    
    webServer.begin(); //start the webserver

}



void loop() {
   //Listen for button presses on the RF remote. If one of these buttons is pressed, feed the chickens the appropriate number of servings using the feedChickens function
   //REMOTE BUTTON 1
   if(digitalRead( buttonPin1 ) == HIGH){
        feedChickens(1);
   }
   //REMOTE BUTTON 2
   if(digitalRead( buttonPin2 ) == HIGH){
        feedChickens(2);
   }
   //REMOTE BUTTON 3
   if(digitalRead( buttonPin3 ) == HIGH){
        feedChickens(3);
   }
   //REMOTE BUTTON 4
   if(digitalRead( buttonPin4 ) == HIGH){
        feedChickens(4);
   }
   //REMOTE BUTTON 5
   if(digitalRead( buttonPin5 ) == HIGH){
        feedChickens(5);
   }
   //REMOTE BUTTON 6
   if(digitalRead( buttonPin6 ) == HIGH){
        feedChickens(6);
   }
   //REMOTE BUTTON 7
   if(digitalRead( buttonPin7 ) == HIGH){
        feedChickens(7);
   }
   //REMOTE BUTTON 8
   if(digitalRead( buttonPin8 ) == HIGH){
        feedChickens(8);
   }
   
   //let's look at the http requests now
   if (webClient.connected() && webClient.available()) {
        while (webClient.connected()) {   
              if (webClient.available()) {
                char c = webClient.read();
             
                //read char by char HTTP request
                if (readString.length() < 100) {
                  //store characters to string
                  readString += c;
                  //Serial.print(c);
                 }
        
                 //if HTTP request has ended
                 if (c == '\n') {
                serveWebpage();
                //look for arguments and feed the chickens appropriately similar to what we did with the RF buttons.
                if (readString.indexOf("?button1") >0){
                       feedChickens(1);
                   }
                if (readString.indexOf("?button2") >0){
                       feedChickens(2);
                   }
                if (readString.indexOf("?button3") >0){
                       feedChickens(3);
                   }
                if (readString.indexOf("?button4") >0){
                       feedChickens(4);
                   }
                if (readString.indexOf("?button5") >0){
                       feedChickens(5);
                   }
                if (readString.indexOf("?button6") >0){
                       feedChickens(6);
                   }
                if (readString.indexOf("?button7") >0){
                       feedChickens(7);
                   }
                if (readString.indexOf("?button8") >0){
                       feedChickens(8);
                   }
                //clear the readString variable so it's empty for the next go around
                readString = "";
                 }
              }
        }
    }
    else {
        webClient = webServer.available();
    }

    //check to see if it's time to close that relay! What we are doing here is turning the relay off if the feeding duration has elapsed
    if ((millis() - relayStartTime) > relayDuration) {
        relayController.turnOffRelay(1);
    }

}

//This function builds the response for the web page which will be displayed to the user in the browser.
void serveWebpage() {
    
    webClient.println("HTTP/1.1 200 OK");
            webClient.println("Content-Type: text/html");
            webClient.println();     
            webClient.println("<HTML>");
            webClient.println("<HEAD>"); 
            webClient.println("<link rel=\"shortcut icon\" href=\"http://joedidit.com/projects/chickenfeeder/favicon.ico\" type=\"image/x-icon\" />");
            webClient.println("<link rel=\"apple-touch-icon\" sizes=\"114×114\" href=\"http://joedidit.com/projects/chickenfeeder/apple-touch-icon-114x114.png\" />");
            webClient.println("<link rel=\"apple-touch-icon\" sizes=\"72×72\" href=\"http://joedidit.com/projects/chickenfeeder/apple-touch-icon-72x72.png\" />");
            webClient.println("<link rel=\"apple-touch-icon\" href=\"http://joedidit.com/projects/chickenfeeder/apple-touch-icon.png\" />");
            webClient.println("<meta name='apple-mobile-web-app-capable' content='yes' />");
            webClient.println("<meta name='apple-mobile-web-app-status-bar-style' content='black' />");
            webClient.println("<style type=\"text/css\">");
            webClient.println("body{ margin:60px 0px; padding:0px; text-align:center;");
            webClient.println("}");
            webClient.println("h1 { text-align: center; font-family:Arial, \"Trebuchet MS\", Helvetica, ");
            webClient.println("sans-serif;");
            webClient.println("}");
            webClient.println("h2 { text-align: center; font-family:Arial, \"Trebuchet MS\", Helvetica, ");
            webClient.println("sans-serif;");
            webClient.println("}");
            webClient.println("a { text-decoration:none; width:75px; height:50px; border-color:black; ");
            webClient.println("border-top:2px solid; border-bottom:2px solid; border-right:2px solid; ");
            webClient.println("border-left:2px solid; border-radius:10px 10px 10px; ");
            webClient.println("-o-border-radius:10px 10px 10px; -webkit-border-radius:10px 10px 10px; ");
            webClient.println("font-family:\"Trebuchet MS\",Arial, Helvetica, sans-serif; ");
            webClient.println("-moz-border-radius:10px 10px 10px; background-color:#293F5E; ");
            webClient.println("padding:8px; text-align:center;");
            webClient.println("}");
            
            webClient.println(".buttonRed { text-decoration:none; width:90px; height:50px; border-color:black; ");
            webClient.println("border-top:2px solid; border-bottom:2px solid; border-right:2px solid; ");
            webClient.println("border-left:2px solid; border-radius:10px 10px 10px; ");
            webClient.println("-o-border-radius:10px 10px 10px; -webkit-border-radius:10px 10px 10px; ");
            webClient.println("font-size: 28px;");
            webClient.println("font-family:\"Trebuchet MS\",Arial, Helvetica, sans-serif; ");
            webClient.println("-moz-border-radius:10px 10px 10px; background-color:#EB190F; ");
            webClient.println("padding:40px; text-align:center;");
            webClient.println("}");

            webClient.println(".buttonYellow { text-decoration:none; width:90px; height:50px; border-color:black; ");
            webClient.println("border-top:2px solid; border-bottom:2px solid; border-right:2px solid; ");
            webClient.println("border-left:2px solid; border-radius:10px 10px 10px; ");
            webClient.println("-o-border-radius:10px 10px 10px; -webkit-border-radius:10px 10px 10px; ");
            webClient.println("font-family:\"Trebuchet MS\",Arial, Helvetica, sans-serif; ");
            webClient.println("font-size: 28px;");
            webClient.println("-moz-border-radius:10px 10px 10px; background-color:#D1CC22; ");
            webClient.println("padding:40px; text-align:center;");
            webClient.println("}");
            
            webClient.println(".buttonGreen { text-decoration:none; width:90px; height:50px; border-color:black; ");
            webClient.println("border-top:2px solid; border-bottom:2px solid; border-right:2px solid; ");
            webClient.println("border-left:2px solid; border-radius:10px 10px 10px; ");
            webClient.println("-o-border-radius:10px 10px 10px; -webkit-border-radius:10px 10px 10px; ");
            webClient.println("font-family:\"Trebuchet MS\",Arial, Helvetica, sans-serif; ");
            webClient.println("font-size: 28px;");
            webClient.println("-moz-border-radius:10px 10px 10px; background-color:#11D51A; ");
            webClient.println("padding:40px; text-align:center;");
            webClient.println("}");
            
            webClient.println(".buttonBlue { text-decoration:none; width:90px; height:50px; border-color:black; ");
            webClient.println("border-top:2px solid; border-bottom:2px solid; border-right:2px solid; ");
            webClient.println("border-left:2px solid; border-radius:10px 10px 10px; ");
            webClient.println("-o-border-radius:10px 10px 10px; -webkit-border-radius:10px 10px 10px; ");
            webClient.println("font-family:\"Trebuchet MS\",Arial, Helvetica, sans-serif; ");
            webClient.println("font-size: 28px;");
            webClient.println("-moz-border-radius:10px 10px 10px; background-color:#157EFC; ");
            webClient.println("padding:40px; text-align:center;");
            webClient.println("}");
            
            webClient.println("a:link {color:white;} /* unvisited link */ a:visited {color:white;} /* ");
            webClient.println("visited link */ a:hover {color:white;} /* mouse over link */ a:active ");
            webClient.println("{color:white;} /* selected link */");
            
            webClient.println("</style>");
            webClient.println("<TITLE>Chicken Feeder</TITLE>");
            webClient.println("</HEAD>");
            webClient.println("<BODY>");
            webClient.println("<H1>Chicken Feeder</H1>");
            webClient.println("<hr />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<a class=\"buttonRed\" href=\"/?button7\"\">7 Servings</a>");
            webClient.println("<a class=\"buttonBlue\" href=\"/?button8\"\">8 Servings</a><br />");   
            webClient.println("<br />");     
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />"); 
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<a class=\"buttonRed\" href=\"/?button5\"\">5 Servings</a>");
            webClient.println("<a class=\"buttonBlue\" href=\"/?button6\"\">6 Servings</a><br />"); 
            webClient.println("<br />");     
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<a class=\"buttonYellow\" href=\"/?button3\"\">3 Servings</a>");
            webClient.println("<a class=\"buttonGreen\" href=\"/?button4\"\">4 Servings</a><br />"); 
            webClient.println("<br />");     
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<br />"); 
            webClient.println("<br />");     
            webClient.println("<br />");
            webClient.println("<a class=\"buttonYellow\" href=\"/?button1\"\">1 Servings</a>");
            webClient.println("<a class=\"buttonGreen\" href=\"/?button2\"\">2 Servings</a><br />"); 
            webClient.println("<br />"); 
            webClient.println("</BODY>");
            webClient.println("</HTML>");
     
           delay(1000);
           //stopping webClient
           webClient.stop();
    delay(100);
}

//This function converts the number of servings into the number of milliseconds the motor needs to run
void feedChickens(long servings) {
    relayController.turnOnRelay(1); //turn on the relay for the motor
    relayDuration = servings * 3000; //sset the amount of time that should pass before turning the relay off.
    relayStartTime = millis(); //set the start time to now.
    
    char publishString[40]; //create a char so we can publisn to the console
    if(Particle.connected() == true){ //only run the next lines if we are connected to the particle cloud
        sprintf(publishString, "%d Servings", servings); //build the publish string
		Particle.publish("Dispensing Feed", publishString); //publish to the cloud
    }
}


//a strtofloat string we use to convert strings to floats
float strToFloat(String str) {
  char carray[str.length() + 1];           // determine size of array
  str.toCharArray(carray, sizeof(carray)); // put str into an array
  return atof(carray);
}

@ParticleD, or @mstanley are you able to assist?

I don’t have insight on this – I haven’t used the TCP Server function. But I’m curious if Joe is the same Joe Vitale that played drums for Joe Walsh back in the 70s :smiley:

1 Like

No advice yet, but I have had success connecting to a Photon from outside via port forwarding.
So it shouldn’t be a limitation on the Photon’s side as such.
Maybe you want to try a simpler TCP or HTTP server to check the fundamentals first.

Ha! No, that’s not me… but one of my college roommates came home with this one evening…

This is me: https://www.instagram.com/mrjoevitale/

This code is for a chicken feeder for our new chicks in our backyard, I have a Blynk version which works flawlessly but I was hoping to build a stand-alone cloudless version also.

I’ll be posting the full project to YouTube shortly.

1 Like

Hi @joevitale

I think another good experiment to run would be to temporarily shutdown one of your existing and working port-forwarded devices and use the static IP and port for that device in your Photon.

That would go a long way to showing where the problem is since if it works, the problem is mostly like in your router, but if it fails then you know it is something unique to the Photon setup.

As @ScruffR said, this works for lots of folks already but there are many variables such as your router, your ISP service etc. I know with my residential ISP, I cannot open arbitrary ports to the outside; only certain services can be open, for instance.

I don’t think this is your problem, but having an incorrect subnet mask set on the Photon can sometimes cause that behavior. The router is able to get a SYN packet to the Photon so the connection opens, but then the Photon can’t send data back.

1 Like

Thank you fur these suggestions!

I’ve tried setting the port to the same port as a working server and unplugged the other device, with the same results.

I should also clarify: sometimes it actually loads the page from outside the network if I repeatedly hit the refresh button in the browser. It will load once, then fails repeatedly, then it’ll load once again.

I have tried this stripped down piece of sample code that I found - it has the same results. I would be curious if anyone else can get it (this code) to work with a port forwarded from the outside.

The routers I use are business class Ubiquiti EdgeRouters - I’ve tried this at two different sites, one of which is on Comcast Business with a static IP.

It’s acting like there’s an IP conflict - but there’s not. Do you know if I have to manually set the mac address for the TCPServer? Both of these networks have large numbers of photons on them - although only this one is running the TCPServer. I’m thinking maybe there are 2 devices with the same MAC (that just seems crazy).

This is a /22 network, but using this sample code (which uses DHCP and should grab the subnet correctly) I have the same issue.

TCPClient webClient;
TCPServer webServer = TCPServer(80);
char myIpAddress[24];

void setup() {
    Particle.variable("ipAddress", myIpAddress, STRING);
    IPAddress myIp = WiFi.localIP();
    sprintf(myIpAddress, "%d.%d.%d.%d", myIp[0], myIp[1], myIp[2], myIp[3]);
    
    webServer.begin();
}

void loop() {
    if (webClient.connected() && webClient.available()) {
        serveWebpage();
    }
    else {
        webClient = webServer.available();
    }
}

void serveWebpage() {
    //TODO: read in the request to see what page they want:
    //TODO: retrieve larger content from flash?
   
    webClient.println("<html>I'm serving a webpage!</html>\n\n");
    webClient.flush();
    webClient.stop();
    delay(100);
}

Forgot to add the code!

TCPClient webClient;
TCPServer webServer = TCPServer(80);
char myIpAddress[24];

void setup() {
    Particle.variable("ipAddress", myIpAddress, STRING);
    IPAddress myIp = WiFi.localIP();
    sprintf(myIpAddress, "%d.%d.%d.%d", myIp[0], myIp[1], myIp[2], myIp[3]);
    
    webServer.begin();
}

void loop() {
    if (webClient.connected() && webClient.available()) {
        serveWebpage();
    }
    else {
        webClient = webServer.available();
    }
}

void serveWebpage() {
    //TODO: read in the request to see what page they want:
    //TODO: retrieve larger content from flash?
   
    webClient.println("<html>I'm serving a webpage!</html>\n\n");
    webClient.flush();
    webClient.stop();
    delay(100);
}

I have a cradlepoint with an AT&T SIM with a static IP in my lab, I could fire it up and see if that yields the same result.

So static IP addresses are very persistent on Photons. When you tested, did you call WiFi.useDynamicIP()? It is not in your code above. Until you explicitly switch back to dynamic, you won’t have valid results for the dynamic IP test.

I think your problem is definitely related to your 22 network (I wish I had noticed that little 252 in your first post). I would try a standard single class C network and see if that fixes your problem.

I will try it on a /24 network and see if that fixes it. If so, then this could be a problem for corporate networks because a /22 isn’t too unusual. I’ll report back!

Okay, so I connected it to my cradlepoint which is connected to the AT&T cell network with a static IP and it works on both a /24 AND a /22 network! Meaning it’s not the photon, but the EdgeRouter.

Inversely, it is the photon and not the EdgeRouter when you consider that everything else on the network with the EdgeRouter works just fine with ports forwarded! :face_with_raised_eyebrow:

Anyhow, I’m unable to change my subnet on the network in question to see if it plays nice with a /24 when used with this type of router. I do have a spare EdgeRouter on a shelf that I may be able to test with one day, but right now I would make a lot of people very unhappy if I knocked them offline to run this test!

Thanks for the help with this!

1 Like