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 2 commits 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
|
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 ? |
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. |
a0dd708 to
54ee20d
Compare
|
@devnexen great catch on the sockopt exception path. You're right that it was inconsistent with For the stream-backed Socket case in opensocket — looking at The Following the same logic I noticed curl_multi_free_obj() had the same exposure: curl_multi_cleanup() closes the multi's pooled connections and fires the close callback of every attached easy handle, and at that point the Two small coverage additions while I was in: a CURL_SOCKOPT_ALREADY_CONNECTED return-value test (asserts the constant flows through to libcurl), and a curl_reset() test that confirms the socket FCC is dropped when the |
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.
- The same teardown is applied to every easy handle attached to a
CurlMultiHandle before curl_multi_cleanup() so the close callback never
fires from within the multi's destructor either.
- Stream-backed Sockets and already-closed Sockets returned from
CURLOPT_OPENSOCKETFUNCTION are refused with a TypeError: a stream-backed
Socket bypasses our bsd_socket detach (socket_free_obj() delegates close
to the backing php_stream), and an already-closed Socket would bury the
real cause under a generic CURLE_COULDNT_CONNECT.
- When the sockopt callback throws, the abort path returns
CURL_SOCKOPT_ERROR (matching opensocket / ssh_hostkey) so libcurl does
not connect with a half-configured socket.
- 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.
54ee20d to
44d3795
Compare
|
looking good in my opinion, note I am only judging the feature itself. |
|
Thanks a lot for your help and great feedbacks @devnexen |
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