diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index 93120e85f..cee91bf2b 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -531,7 +531,54 @@ func (t *Wrapper) Write(buf []byte, offset int) (int, error) { } t.noteActivity() - return t.tdev.Write(buf, offset) + return t.write(buf, offset) +} + +func (t *Wrapper) write(buf []byte, offset int) (int, error) { + if t.ring == nil { + return t.tdev.Write(buf, offset) + } + + // below copied from wireguard-go NativeTUN.Write + + // reserve space for header + buf = buf[offset-4:] + + // add packet information header + buf[0] = 0x00 + buf[1] = 0x00 + if buf[4]>>4 == ipv6.Version { + buf[2] = 0x86 + buf[3] = 0xdd + } else { + buf[2] = 0x08 + buf[3] = 0x00 + } + + n, err := t.ring.Write(buf) + if errors.Is(err, syscall.EBADFD) { + err = os.ErrClosed + } + return n, err +} + +func (t *Wrapper) read(buf []byte, offset int) (n int, err error) { + // TODO: upstream has graceful shutdown error handling here. + buff := buf[offset-4:] + if uring.URingAvailable() { + n, err = t.ring.Read(buff[:]) + } else { + n, err = t.tdev.(*wgtun.NativeTun).File().Read(buff[:]) + } + if errors.Is(err, syscall.EBADFD) { + err = os.ErrClosed + } + if n < 4 { + n = 0 + } else { + n -= 4 + } + return } func (t *Wrapper) GetFilter() *filter.Filter { diff --git a/net/uring/.gitignore b/net/uring/.gitignore new file mode 100644 index 000000000..7985da7a9 --- /dev/null +++ b/net/uring/.gitignore @@ -0,0 +1,2 @@ +liburing/ +*.so diff --git a/net/uring/io_uring.c b/net/uring/io_uring.c index 2a3c2aa2f..387b2ddb4 100644 --- a/net/uring/io_uring.c +++ b/net/uring/io_uring.c @@ -1,3 +1,7 @@ +// +build linux + +#if __has_include() + #include // debugging #include #include @@ -7,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -160,3 +165,13 @@ static go_completion_result completion(struct io_uring *ring, int block) { io_uring_cqe_seen(ring, cqe); return res; } + +#endif + +static int has_io_uring(void) { + #if __has_include() + return 1; + #else + return 0; + #endif +} diff --git a/net/uring/io_uring.go b/net/uring/io_uring.go new file mode 100644 index 000000000..12a49ef58 --- /dev/null +++ b/net/uring/io_uring.go @@ -0,0 +1,14 @@ +package uring + +import ( + "errors" + "flag" +) + +var useIOURing = flag.Bool("use-io-uring", true, "attempt to use io_uring if available") + +// NotSupportedError indicates an operation was attempted when io_uring is not supported. +var NotSupportedError = errors.New("io_uring not supported") + +// DisabledError indicates that io_uring was explicitly disabled. +var DisabledError = errors.New("io_uring disabled") diff --git a/net/uring/io_uring_linux.go b/net/uring/io_uring_linux.go index f36c1dd96..dfaa1d031 100644 --- a/net/uring/io_uring_linux.go +++ b/net/uring/io_uring_linux.go @@ -1,6 +1,7 @@ package uring -// #cgo LDFLAGS: -luring +// #cgo CFLAGS: -I${SRCDIR}/liburing/src/include +// #cgo LDFLAGS: -L${SRCDIR}/liburing/src/ -luring // #include "io_uring.c" import "C" @@ -26,6 +27,8 @@ const bufferSize = device.MaxSegmentSize +func URingAvailable() bool { return *useIOURing && C.has_io_uring() > 0 } + // A UDPConn is a recv-only UDP fd manager. // We'd like to enqueue a bunch of recv calls and deqeueue them later, // but we have a problem with buffer management: We get our buffers just-in-time @@ -68,18 +71,31 @@ type UDPConn struct { } func NewUDPConn(pconn net.PacketConn) (*UDPConn, error) { + if !*useIOURing { + return nil, DisabledError + } conn, ok := pconn.(*net.UDPConn) if !ok { return nil, fmt.Errorf("cannot use io_uring with conn of type %T", pconn) - } + } // this is dumb - local := conn.LocalAddr().String() - ip, err := netaddr.ParseIPPort(local) - if err != nil { - return nil, fmt.Errorf("failed to parse UDPConn local addr %s as IP: %w", local, err) + local := conn.LocalAddr() + var ipp netaddr.IPPort + switch l := local.(type) { + case *net.UDPAddr: + ip, ok := netaddr.FromStdIP(l.IP) + if !ok { + return nil, fmt.Errorf("failed to parse IP: %v", ip) + } + ipp = netaddr.IPPortFrom(ip, uint16(l.Port)) + default: + var err error + if ipp, err = netaddr.ParseIPPort(l.String()); err != nil { + return nil, fmt.Errorf("failed to parse UDPConn local addr %s as IP: %w", local, err) + } } ipVersion := 6 - if ip.IP().Is4() { + if ipp.IP().Is4() { ipVersion = 4 } // TODO: probe for system capabilities: https://unixism.net/loti/tutorial/probe_liburing.html diff --git a/net/uring/io_uring_notlinux.go b/net/uring/io_uring_notlinux.go new file mode 100644 index 000000000..098e4ac9d --- /dev/null +++ b/net/uring/io_uring_notlinux.go @@ -0,0 +1,56 @@ +// +build !linux + +package uring + +import ( + "net" + "os" + "time" + + "inet.af/netaddr" +) + +func URingAvailable() bool { return false } + +// A UDPConn is a recv-only UDP fd manager. +// TODO: Support writes. +// TODO: support multiplexing multiple fds? +// May be more expensive than having multiple urings, and certainly more complicated. +// TODO: API review for performance. +// We'd like to enqueue a bunch of recv calls and deqeueue them later, +// but we have a problem with buffer management: We get our buffers just-in-time +// from wireguard-go, which means we have to make copies. +// That's OK for now, but later it could be a performance issue. +// For now, keep it simple and enqueue/dequeue in a single step. +// TODO: IPv6 +// TODO: Maybe combine the urings into a single uring with dispatch. +type UDPConn struct{} + +func NewUDPConn(conn *net.UDPConn) (*UDPConn, error) { return nil, NotSupportedError } +func (c *UDPConn) LocalAddr() net.Addr { panic("Not supported") } + +func (u *UDPConn) Close() error { return NotSupportedError } +func (c *UDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + err = NotSupportedError + return +} +func (u *UDPConn) ReadFromNetaddr(buf []byte) (n int, addr netaddr.IPPort, err error) { + err = NotSupportedError + return +} +func (u *UDPConn) WriteTo(p []byte, addr net.Addr) (int, error) { return 0, NotSupportedError } +func (c *UDPConn) SetDeadline(t time.Time) error { return NotSupportedError } +func (c *UDPConn) SetReadDeadline(t time.Time) error { return NotSupportedError } +func (c *UDPConn) SetWriteDeadline(t time.Time) error { return NotSupportedError } + +// A File is a write-only file fd manager. +// TODO: Support reads +// TODO: all the todos from UDPConn +type File struct{} + +func NewFile(file *os.File) (*File, error) { return nil, NotSupportedError } +func (u *File) Write(buf []byte) (int, error) { return 0, NotSupportedError } + +// Read data into buf[offset:]. +// We are allowed to write junk into buf[offset-4:offset]. +func (u *File) Read(buf []byte) (n int, err error) { return 0, NotSupportedError } diff --git a/net/uring/io_uring_test.go b/net/uring/io_uring_test.go new file mode 100644 index 000000000..3c0b9187c --- /dev/null +++ b/net/uring/io_uring_test.go @@ -0,0 +1,30 @@ +// +build linux + +package uring + +import ( + "io/ioutil" + "testing" +) + +func TestFile(t *testing.T) { + tmpFile, err := ioutil.TempFile(".", "uring-test") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + f, err := NewFile(tmpFile) + if err != nil { + t.Fatalf("failed to create io_uring file: %v", err) + } + content := []byte("a test string to check writing works 😀 with non-unicode input") + n, err := f.Write(content) + if n != len(content) { + t.Errorf("mismatch between reported written len and content len: want %d, got %d", len(content), n) + } + if err != nil { + t.Errorf("file write failed: %v", err) + } + if err = f.Close(); err != nil { + t.Errorf("file close failed: %v", err) + } +} diff --git a/net/uring/makefile b/net/uring/makefile new file mode 100644 index 000000000..b43d82f7b --- /dev/null +++ b/net/uring/makefile @@ -0,0 +1,3 @@ +get_liburing: + git clone git@github.com:axboe/liburing.git + cd liburing && make