Why doesn't getsockopt take as a parameter the buffer to pass to the C-extended getsockopt

Hello,

I need to make getsockopt calls from Python, however the calls I intended to make require that the buffer passed to the C-extended getsockopt function be initialized in a certain way. The Python’s API is not designed as so, it only takes as a parameter (level, optname, bufflen). I’d like to extend to (level, optname, buff, bufflen). Is there any reason to why Python didn’t want to take a buffer from Python directly ?

Thank you in advance for you help

The main reason is: you almost never need that, and it’s a pain to have to pass buffers around. Can you give an example of where the input buffer needs to be pre-initialized? Preferably a pair of examples, one in C one in Python, showing different behaviour?

Thank for you reply,

Here’s a C code snippet :

int main(int argc, char const *argv[])
{
    /* code */
    int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP);
    struct mptcp_subflow_data inf;
    memset(&inf, 0, sizeof(inf));
    inf.size_subflow_data = sizeof(struct mptcp_subflow_data);
    socklen_t optlen = sizeof(inf);
	/* We should get EOPNOTSUPP in case of fallback */
	if (getsockopt(fd, SOL_MPTCP, 2, &inf, &optlen) < 0)
		return 1;
	printf("The number of subflows used are %d\n", inf.num_subflows);
    return 0;
}

The most reassembling python code I could come up :

print(struct.calcsize("=IIII"))
returned = sock.getsockopt(284, 2, 16)
print(struct.unpack("=IIII", returned))

Note that this doesn’t work because the kernel expects the buffer passed to getsocketmodule to be initialized in a certain way. But the buffer passed on in the socketmodule.c from CPython doesn’t do such a thing.

With the above idea, we would have getsockopt that looks like this :

byte_object = struct.pack("=IIII", 16, 0, 0, 0)
returned = sock.getsockopt(284, 2, byte_object, 16)
print(struct.unpack("=IIII", returned))

Thank you in advance for your reply

Hmm. Unfortunately I can’t test this; it fails in compilation, which I suspect means that Linux 5.10.158 doesn’t have SOL_MPTCP support. Either that, or it’s a massive case of PEBCAK and I just messed up which headers to include, but I grepped all my headers for that identifier and didn’t find any. Is this an incredibly new feature, or am I derping badly?

This is the same case we already discussed and you agreed was a mistaken analysis on your part?

@barry-scott Actually, the error on my part was about the fact that buffer should be all zeroed out. However, we need to zero out a part of the buffer and set another part of the buffer to a specific value, as showed in the C example. The kernel reads from this buffer.

@Rosuav
Here’s the full C code in order to test it out :

#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdbool.h>
#include <linux/tcp.h>
#include <linux/types.h>

#ifndef SOL_MPTCP
#define SOL_MPTCP 284
#endif

#define IPPROTO_MPTCP 262
#define MPTCPLIB_OPERATION_UNSUPPORTED -2
#define MPTCPLIB_SOCKET_FALLBACK_TCP -1
#define MPTCPLIB_ERROR_FLAG -3

#ifndef _UAPI_MPTCP_H

#define MPTCP_INFO 		1
#define MPTCP_TCPINFO	2

struct mptcp_subflow_data {
	__u32		size_subflow_data;		/* size of this structure in userspace */
	__u32		num_subflows;			/* must be 0, set by kernel */
	__u32		size_kernel;			/* must be 0, set by kernel */
	__u32		size_user;				/* size of one element in data[] */
} __attribute__((aligned(8)));

#endif

int main(int argc, char const *argv[])
{
    int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP);
    struct mptcp_subflow_data inf;
    memset(&inf, 0, sizeof(inf)); // If you comment out this part then you get an error
    inf.size_subflow_data = sizeof(struct mptcp_subflow_data);
    socklen_t optlen = sizeof(inf);
	if (getsockopt(fd, SOL_MPTCP, 2, &inf, &optlen) < 0){
		perror("The error");
		return 1;
	}
	printf("The number of subflows used are %d\n", inf.num_subflows);
    return 0;
}

Man 2 getsockopt says that the buffer is output only as i read the docs.

https://man7.org/linux/man-pages/man2/getsockopt.2.html

Thanks for posting the code. I still can’t test this reliably though - I get “Bad file descriptor” whether the line is commented out or not. Adding a perror just after socket creation shows “Protocol not supported”.

Given that you’re defining a lot of the constants manually, I have to ask: is this a non-standard feature that works only on certain network devices? Maybe the driver is misbehaving by assuming that it can read from the socket option buffer.

1 Like

I found this checklist for multi path TCP

I’m defining certain constants cause the protocol hasn’t been widely deployed, but it is indeed implemented in the Linux kernel from 5.6, for some operations it requires a kernel >= 5.16

You could use ctypes to do the getsockopt with a buffer you supply.

1 Like

@barry-scott thanks for you reply.
Can you provide an example, please ?

Not off the top of my head. I’m sure you can code this for yourself
given you are a C programmer. The docs for ctypes are very good.

Once you find the .so that getsockopt is in then the rest should not be difficult.
Ask here if you hit an issue.

@barry-scott I agree with you thet one can use struct or ctypes libs to pass a buffer from python, but in my use-case I wanted to know if there’s already a python implementation of the desired functionality.

In case not, (which seems to be the case) I wanted to know why they didn’t make the getsockopt interface as the one in C. Since I’ll have to make my own C-extension.

FWIW, os.read(fd, n, /) is similar. I think it’s for convenience, so that users don’t need to allocate a buffer themselves, and it supports chain-calling such as print(os.read(fd, 16).decode(), int(os.read(fd, 16))). OTOH the disadvantage is obvious, it’s impossible to provide your own buffer.

For os.read, the solution is io.FileIO.readinto: io - python read from fd directly into bytearray - Stack Overflow. In 2015, it was suggested to add a os.read_into function: Add a new os.read_into() function to avoid memory copies · Issue #67942 · python/cpython · GitHub, but the issue was closed due to lack of interest.

1 Like

For simple stuff like this i tend to use ctypes and avoid the overhead of maintaining a C or C++ extension. And I speak as the maintainer of PyCXX.

1 Like

@barry-scott Thank you for your insight, it is indeed a direction that I’m going to take. @GalaxySnail thank you for your insight and comment.

In case you haven’t used ctypes before, here’s an example to get you started.

The getsockopt() function should be available in the global symbol table, i.e. ctypes.CDLL(None)[1]. You can capture errno for calls by specifying use_errno=True. The last captured value of errno for the current thread is available as ctypes.get_errno(). The number and types of function parameters are specified via the argtypes attribute of a function pointer.

import os
import socket

SOL_MPTCP = 284
IPPROTO_MPTCP = 262
MPTCPLIB_OPERATION_UNSUPPORTED = -2
MPTCPLIB_SOCKET_FALLBACK_TCP = -1
MPTCPLIB_ERROR_FLAG = -3
MPTCP_INFO = 1
MPTCP_TCPINFO = 2

try:
    import ctypes

    # distinct socklen_t type
    class socklen_t(ctypes.c_uint32):
        pass

    # alias socklen_t type
    # socklen_t = ctypes.c_uint32

    class mptcp_subflow_data(ctypes.Structure):
        _fields_ = (
            ('size_subflow_data', ctypes.c_uint32), # this structure size
            ('num_subflows', ctypes.c_uint32),      # must be 0
            ('size_kernel', ctypes.c_uint32),       # must be 0
            ('size_user', ctypes.c_uint32),         # element size in data[]
        )

        __slots__ = ()

        def __init__(self, size_user=0):
            super().__init__(size_user=size_user)
            self.size_subflow_data = ctypes.sizeof(self)

    getsockopt = ctypes.CDLL(None, use_errno=True).getsockopt
    getsockopt.restype = ctypes.c_int
    getsockopt.argtypes = (
        ctypes.c_int,      # sockfd
        ctypes.c_int,      # level
        ctypes.c_int,      # optname
        ctypes.c_void_p,   # optval
        ctypes.POINTER(socklen_t), # optlen
    )

except (ImportError, TypeError, AttributeError):
    getsockopt = None

Usage:

    if getsockopt is None:
        raise OSError('platform not supported')
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, IPPROTO_MPTCP)
    optval = mptcp_subflow_data()
    optlen = socklen_t(ctypes.sizeof(optval))
    if getsockopt(s.fileno(), SOL_MPTCP, 2, ctypes.byref(optval),
                  ctypes.byref(optlen)) == -1:
        errno = ctypes.get_errno()
        raise OSError(errno, os.strerror(errno))

    print(f"The number of subflows used is {optval.num_subflows}.")

  1. On Windows this raises TypeError, but only Linux matters here. ↩︎

2 Likes

@eryksun I can’t even thank you enough, I’ll still check ctypes because it seems like a good alternative (in some cases) to C-extensions.