Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UPnP support (port forwarding, querying external IP) #18780

Merged
merged 1 commit into from
Jun 7, 2018

Conversation

mhilbrunner
Copy link
Member

@mhilbrunner mhilbrunner commented May 11, 2018

This PR adds support for Universal Plug and Play (UPnP) to Godot's multiplayer capabilities.

This allows:

  1. To forward ports automatically when hosting games behind a router, instead of requiring the user to do it themselves
  2. To query the router for the external IP address

This is based on the BSD-licensed miniupnpc. The only change to the upstream files was an additional header file that would usually be autogenerated with cmake. In our case, I added it manually.

Changes

  • Adds miniupnc under thirdparty/miniupnpc
  • Adds Godot implementation under modules/upnp
  • Adds settings for module to scons

Simplest example

# Forward external UDP port 1234 on the router to us
var upnp = UPNP.new()
upnp.discover()
upnp.add_port_mapping(1234)
# Do something with the opened port, then on quitting:
upnp.delete_port_mapping(1234)

This autodetects the router and creates (or overwrites) the port mapping.
You can also customize the UPnP discovery params (by calling upnp.discover() yourself) and the way the gateway is chosen by using the methods on UPNP to iterate the UPNPDevices. Once you found the device of your choosing, you can then call add_port_mapping() and delete_port_mapping() on that device, again providing the parameters of your choice. The methods on UPNP are just shortcuts. 😄

Of course, you can fine tune all relevant settings (like timeouts/ttl, internal and external ports, TCP vs UDP, lease time, description for the port forward etc.) from GDScript.

Glossary
IGD: Internet Gateway Device, the UPnP type for routers
NAT: Network address translation
UPnP: Universal Plug and Play, the protocol

Notes
Not all routers are UPnP compatible. Not all devices support wildcards in port mappings, or a non-infinite duration for the lease, or a description. Look at this list to get an idea of how varied UPnP support is. The default parameters as they are now should maximize compatibility.

Relevant to #13947.
Closes #2970.

@mhilbrunner mhilbrunner requested a review from Faless May 11, 2018 00:27
@mhilbrunner mhilbrunner force-pushed the upnp branch 2 times, most recently from 0b12347 to 6dbf050 Compare May 11, 2018 00:47
@akien-mga akien-mga added this to the 3.1 milestone May 11, 2018
@akien-mga
Copy link
Member

It fails building on Linux and macOS:

modules/upnp/upnp.cpp:115:38: error: use of undeclared identifier 'malloc'; did you mean 'alloca'?
        struct UPNPUrls* urls = (UPNPUrls *)malloc(sizeof(struct UPNPUrls));
                                            ^~~~~~
                                            alloca
/usr/include/alloca.h:32:14: note: 'alloca' declared here
extern void *alloca (size_t __size) __THROW;
             ^
modules/upnp/upnp.cpp:122:38: error: use of undeclared identifier 'malloc'; did you mean 'alloca'?
        struct IGDdatas* data = (IGDdatas *)malloc(sizeof(struct IGDdatas));
                                            ^~~~~~
                                            alloca
/usr/include/alloca.h:32:14: note: 'alloca' declared here
extern void *alloca (size_t __size) __THROW;
             ^
modules/upnp/upnp.cpp:133:2: error: use of undeclared identifier 'free'; did you mean 'DefaultAllocator::free'?
        free(xml);
        ^~~~
        DefaultAllocator::free
core/os/memory.h:68:29: note: 'DefaultAllocator::free' declared here
        _FORCE_INLINE_ static void free(void *p_ptr) { return Memory::free_static(p_ptr, false); }
                                   ^
3 errors generated.

Shouldn't you use the Godot memory management methods (memnew, memfree, memalloc)?

@mhilbrunner mhilbrunner force-pushed the upnp branch 2 times, most recently from 6931f40 to 1676a54 Compare May 11, 2018 14:48
@mhilbrunner
Copy link
Member Author

mhilbrunner commented May 11, 2018

@akien-mga Fixed the build, was missing an include. Can't use Godot's memory management functions as the memory in question is passed around to the library and/or either allocated or freed by it.

private:
String discover_multicast_if;
int discover_local_port;
bool discover_ipv6;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPNP class is missing a contructor where variable are not properly initialized. This causes them to have random initial values causing apparently random failure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed :)

@mhilbrunner mhilbrunner force-pushed the upnp branch 4 times, most recently from 81ff2f2 to 073b6c2 Compare May 14, 2018 03:23
@mhilbrunner mhilbrunner force-pushed the upnp branch 6 times, most recently from 101866c to 7fdac09 Compare June 5, 2018 18:11
@mhilbrunner
Copy link
Member Author

mhilbrunner commented Jun 5, 2018

Added docs.

Will see about adding support for PCP and NAT-PMP in a seperate PR (using the BSD-licensed https://github.com/libpcp/pcp).

Then I'd like to add a "PortForwarder" Godot node on top of these that manages port forwards.
You'd add that to your multiplayer/lobby scene and configure the ports you wish to forward.
It would then automatically forward those when the scene is run and remove the mapping on exit, handle threading and use PCP/NAT-PMP and fallback to UPnP if those didn't work.

You can of course also use the UPNP/UPNPDevice classes directly, if you want to do something custom. You just need to handle threading yourself or live with blocking, which may be fine if done at startup/shutdown or during loading.
A little like HTTPRequest vs HTTPClient.

Copy link
Collaborator

@Faless Faless left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job 🥇 !
There are 2 small issues to fix (see comments).
And the question about javascript is open.

I've tested builds with my usual library schedule (X11-Linux/Server-Linux/Server-OpenBSD/Server-FreeBSD/OSX/iPhone/Android/Win/UWP/Javascript), and they all compile fine.
Also, port mapping seems to okay with my home router (had to enable IGDevice on it because it was factory disabled), as even though I can't see them in the usual router UI the port does get open.

Thank you for this PR! It is almost ready to merge :)

@@ -0,0 +1,14 @@
def can_build(platform):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be:

def can_build(env, platform): 

Copy link
Collaborator

@Faless Faless Jun 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also disable it for javascript platform? It doesn't make much sense there, but would it break exports? I never fully understood that, but it is done for things like openSSL/mbedTLS

EDIT: No, now that I think of it is not safe, it works for openSSL/mbedTLS because we provide an empty interface for that. I guess we can build it in javascript for now, maybe add an empty interface for it in the future

int UPNPDevice::add_port_mapping(int port, int port_internal, String desc, String proto, int duration) const {
ERR_FAIL_COND_V(!is_valid_gateway(), UPNP::UPNP_RESULT_INVALID_GATEWAY);
ERR_FAIL_COND_V(port < 1 || port > 65535, UPNP::UPNP_RESULT_INVALID_PORT);
ERR_FAIL_COND_V(port < 0 || port > 65535, UPNP::UPNP_RESULT_INVALID_PORT); // Needs to allow 0 because 0 signifies "use external port as internal port"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are doing both port < 1 and port < 0 check. Did you want to check for port_internal instead 😉 ?

@Faless Faless merged commit b4c6509 into godotengine:master Jun 7, 2018
@mhilbrunner mhilbrunner deleted the upnp branch June 7, 2018 00:13
@akien-mga
Copy link
Member

Only thing missing was to link against system miniupnpc on linuxbsd if builtin_upnpc=no, but I'll add that :)

@akien-mga
Copy link
Member

And also missing adding the license details to COPYRIGHT.txt. I guess we should add this CODEOWNERS file and put me for thirdparty/* ;)

akien-mga pushed a commit that referenced this pull request Jun 27, 2019
I have added a small code sample to show how to automate the process of port forwarding (and closing after you are done).
Thanks to @mhilbrunner for explaining how to achieve t0his (see #18780)
akien-mga pushed a commit that referenced this pull request Jul 29, 2019
I have added a small code sample to show how to automate the process of port forwarding (and closing after you are done).
Thanks to @mhilbrunner for explaining how to achieve t0his (see #18780)

(cherry picked from commit 2a63853)
myhalibobo pushed a commit to myhalibobo/godot that referenced this pull request Sep 3, 2019
I have added a small code sample to show how to automate the process of port forwarding (and closing after you are done).
Thanks to @mhilbrunner for explaining how to achieve t0his (see godotengine#18780)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Peer-to-Peer across NAT
3 participants