// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

//go:build linux

package netutil

import (
	"errors"
	"net"
	"syscall"
	"time"

	"golang.org/x/sys/unix"
)

// SetUserTimeout sets the TCP_USER_TIMEOUT setting on the provided conn.
func SetUserTimeout(conn *net.TCPConn, timeout time.Duration) error {
	// By default from Go, keep alive period + idle are ~15sec. The default
	// keep count is 8 according to some kernel docs. That means it should
	// fail after ~120 seconds. Unfortunately, keep alive only happens if
	// there is no send-q on the socket, and so a slow reader can still cause
	// hanging sockets forever. By setting user timeout, we will kill the
	// connection if any writes go unacknowledged for the amount of time.
	// This should close the keep alive hole.
	//
	// See https://blog.cloudflare.com/when-tcp-sockets-refuse-to-die/

	rawConn, err := conn.SyscallConn()
	if err != nil {
		return err
	}
	controlErr := rawConn.Control(func(fd uintptr) {
		err = unix.SetsockoptInt(int(fd), unix.SOL_TCP, unix.TCP_USER_TIMEOUT, int(timeout.Milliseconds()))
	})
	if controlErr != nil {
		return controlErr
	}
	if ignoreProtocolNotAvailable(err) != nil {
		return err
	}
	return nil
}

// ignoreProtocolNotAvailable ignores the "protocol not available" error that
// is returned when netutil.SetUserTimeout is called if running on the Windows
// Subsystem for Linux (see Jira issue COM-23).
func ignoreProtocolNotAvailable(err error) error {
	var errno syscall.Errno
	if errors.As(err, &errno) {
		if errno == syscall.ENOPROTOOPT {
			return nil
		}
	}
	return err
}
