ext/curl: add socket callback options bridging to ext/sockets#22159
ext/curl: add socket callback options bridging to ext/sockets#22159xavierleune wants to merge 1 commit into
Conversation
e34e404 to
3ed7c46
Compare
|
@Girgias 👋 hi gina, do you mind having a look on this one ? |
Girgias
left a comment
There was a problem hiding this comment.
This looks sensible, although this probably would only ever work if ext/socket (and possibly ext/curl) are built statically into PHP. Which might be a problem for distributions.
I'm not really an expert in how to determine and handle optional dependencies, especially if they can be shared objects. So maybe @devnexen or @remicollet have pointers?
|
I m not entirely certain that the ZEND_MOD_REQUIRED approach is necessarily the best in that case ... but I may look more into the sockets part itself and leave the dependency aspect to Remi, he probably knows better. |
3ed7c46 to
33de0ab
Compare
| } | ||
|
|
||
| echo "\nTesting with invalid return value\n"; | ||
| curl_setopt($ch, CURLOPT_SOCKOPTFUNCTION, function ($ch, $socket, $purpose) { |
There was a problem hiding this comment.
I did not look yet in detail but what happen if you add case where it actually throws an exception ?
|
Instinctively speaking, I think this PR is fine (at least feature wise). However, I would like to see more test cases. e.g. what happens when you set TCP_NODELAY while curl se CURLOPT_TCP_NODELAY. What happens if you set the socket to blocking mode, what curl does ? |
Expose libcurl's CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and
CURLOPT_CLOSESOCKETFUNCTION, letting userland hook into socket creation,
configuration and teardown. These are useful for application security, in
particular SSRF protection (validating the resolved address before connecting)
and low-level socket hardening.
Following the existing curl_write_header / curl_prereqfunction bridge model, the
callbacks exchange ext/sockets Socket objects so they are fully usable in pure
PHP (socket_create/socket_bind, socket_set_option, socket_close):
- sockopt: fn(CurlHandle $ch, Socket $socket, int $purpose): int
returns CURL_SOCKOPT_OK / _ERROR / _ALREADY_CONNECTED
- opensocket: fn(CurlHandle $ch, int $purpose, array $address): Socket|false
$address = [family, socktype, protocol, ip, port];
returning false aborts the connection (CURL_SOCKET_BAD)
- closesocket: fn(CurlHandle $ch, Socket $socket): void
The dependency on ext/sockets is optional both at build and at runtime
(ZEND_MOD_OPTIONAL): the rest of ext/curl keeps working when sockets is not
loaded, and the three socket-callback options simply throw a clear Error when
invoked in that configuration. The Socket class entry is resolved lazily by
name at MINIT rather than referenced as a link-time symbol, so curl never
carries a hard symbol dependency on ext/sockets.
Notable details:
- Descriptors owned by libcurl are detached (bsd_socket = -1) before the
temporary Socket object is released, to avoid a double close. Socket
objects are created without calling socket_import_file_descriptor(), which
on Windows emits a spurious WSAEINVAL warning on a not-yet-connected
socket during the SOCKOPT phase.
- Pooled connections still alive at curl_easy_cleanup() would otherwise
invoke the userland CURLOPT_CLOSESOCKETFUNCTION callback during handle
destruction. Calling into PHP from there is unsafe (an exception thrown
from the callback would surface outside any try/catch). Setting the
option back to NULL on the easy handle is not enough — libcurl caches the
function pointer per connection. The close FCC is torn down before
curl_easy_cleanup() so the trampoline falls through to its native-close
fallback when libcurl invokes it.
- Setting an option to null restores libcurl's native default.
New constants (only defined when ext/curl is built with sockets headers
available, i.e. HAVE_SOCKETS): CURLOPT_SOCKOPTFUNCTION,
CURLOPT_OPENSOCKETFUNCTION, CURLOPT_CLOSESOCKETFUNCTION, CURL_SOCKOPT_OK,
CURL_SOCKOPT_ERROR, CURL_SOCKOPT_ALREADY_CONNECTED, CURLSOCKTYPE_IPCXN,
CURLSOCKTYPE_ACCEPT.
33de0ab to
c576217
Compare
|
@devnexen new test cases added, with a little fix around socket closing. About custom options, libcurl seems to apply it's own option after the callback returns. So the implementation seems robust. |
Summary
This PR exposes libcurl's three socket-level callback options, which were previously unavailable in PHP:
CURLOPT_SOCKOPTFUNCTION— invoked after a socket is created but before it is connected, to tune low-level socket options.CURLOPT_OPENSOCKETFUNCTION— invoked to create the socket for a connection, after the address has been resolved but beforeconnect().CURLOPT_CLOSESOCKETFUNCTION— invoked when libcurl is done with a socket.The main motivation is application security.
CURLOPT_OPENSOCKETFUNCTIONin particular lets an application inspect the resolved IP address and refuse the connection, which is the robust way to implement SSRF protection (it happens after DNS resolution, so it also defeats DNS-rebinding to internal addresses — something hostname/URL allow-listing cannot do).CURLOPT_SOCKOPTFUNCTIONallows socket hardening (SO_BINDTODEVICE, keep-alive, packet marks, …).API
The C callbacks take/return a raw
curl_socket_tfile descriptor, which pure PHP cannot create or read. To make the options usable from plain PHP, the callbacks exchange ext/socketsSocketobjects, built on top of the C API alreadyexported by ext/sockets (
socket_ce, thephp_socketstruct andsocket_import_file_descriptor()):Example: blocking private/reserved IPs (SSRF protection)
Because
CURLOPT_OPENSOCKETFUNCTIONreceives the resolved address, it can reject any request that would reach a private or reserved range — even if the attacker supplied a public hostname that resolves to an internal IP:Implementation notes
HAVE_SOCKETS. ext/curl gains aZEND_MOD_REQUIRED("sockets")dependency (plusPHP_ADD_EXTENSION_DEP); when built without ext/sockets, curl still builds, just without these options.bsd_socket = -1) before the temporarySocketobject is released, to avoid a double close.CURLOPT_CLOSESOCKETFUNCTIONis disabled right beforecurl_easy_cleanup(), so libcurl closes pooled sockets natively instead of calling into PHP while the handle is being destroyed (which can happen during GC/shutdown). The callback still fires for sockets closed during a transfer.nullrestores libcurl's native default.CURLOPT_SOCKOPTFUNCTION,CURLOPT_OPENSOCKETFUNCTION,CURLOPT_CLOSESOCKETFUNCTION,CURL_SOCKOPT_OK,CURL_SOCKOPT_ERROR,CURL_SOCKOPT_ALREADY_CONNECTED,CURLSOCKTYPE_IPCXN,CURLSOCKTYPE_ACCEPT.Tests
Added
.phptcoverage for each option (success, abort/error paths, invalid return types/values,null,curl_copy_handle) plus a trampoline test. The fullext/curlsuite passes with no regressions.Fixes: https://bugs.php.net/bug.php?id=62906