]> Sergey Matveev's repositories - btrtrc.git/blobdiff - socket.go
Drop support for go 1.20
[btrtrc.git] / socket.go
index 8da07d2ba8c3fce01c79fe1196355c5f39a9e53f..2d4ea863ac292f39e4d97effd297fe2b98afcfa5 100644 (file)
--- a/socket.go
+++ b/socket.go
@@ -4,7 +4,9 @@ import (
        "context"
        "net"
        "strconv"
+       "syscall"
 
+       "github.com/anacrolix/log"
        "github.com/anacrolix/missinggo/perf"
        "github.com/anacrolix/missinggo/v2"
        "github.com/pkg/errors"
@@ -24,26 +26,84 @@ type socket interface {
        Close() error
 }
 
-func listen(n network, addr string, f firewallCallback) (socket, error) {
+func listen(n network, addr string, f firewallCallback, logger log.Logger) (socket, error) {
        switch {
        case n.Tcp:
                return listenTcp(n.String(), addr)
        case n.Udp:
-               return listenUtp(n.String(), addr, f)
+               return listenUtp(n.String(), addr, f, logger)
        default:
                panic(n)
        }
 }
 
+// Dialing TCP from a local port limits us to a single outgoing TCP connection to each remote
+// client. Instead, this should be a last resort if we need to use holepunching, and only then to
+// connect to other clients that actually try to holepunch TCP.
+const dialTcpFromListenPort = false
+
+var tcpListenConfig = net.ListenConfig{
+       Control: func(network, address string, c syscall.RawConn) (err error) {
+               controlErr := c.Control(func(fd uintptr) {
+                       if dialTcpFromListenPort {
+                               err = setReusePortSockOpts(fd)
+                       }
+               })
+               if err != nil {
+                       return
+               }
+               err = controlErr
+               return
+       },
+       // BitTorrent connections manage their own keep-alives.
+       KeepAlive: -1,
+}
+
 func listenTcp(network, address string) (s socket, err error) {
-       l, err := net.Listen(network, address)
-       return tcpSocket{
+       l, err := tcpListenConfig.Listen(context.Background(), network, address)
+       if err != nil {
+               return
+       }
+       netDialer := net.Dialer{
+               // We don't want fallback, as we explicitly manage the IPv4/IPv6 distinction ourselves,
+               // although it's probably not triggered as I think the network is already constrained to
+               // tcp4 or tcp6 at this point.
+               FallbackDelay: -1,
+               // BitTorrent connections manage their own keepalives.
+               KeepAlive: tcpListenConfig.KeepAlive,
+               Control: func(network, address string, c syscall.RawConn) (err error) {
+                       controlErr := c.Control(func(fd uintptr) {
+                               err = setSockNoLinger(fd)
+                               if err != nil {
+                                       // Failing to disable linger is undesirable, but not fatal.
+                                       log.Levelf(log.Debug, "error setting linger socket option on tcp socket: %v", err)
+                                       err = nil
+                               }
+                               // This is no longer required I think, see
+                               // https://github.com/anacrolix/torrent/discussions/856. I added this originally to
+                               // allow dialling out from the client's listen port, but that doesn't really work. I
+                               // think Linux older than ~2013 doesn't support SO_REUSEPORT.
+                               if dialTcpFromListenPort {
+                                       err = setReusePortSockOpts(fd)
+                               }
+                       })
+                       if err == nil {
+                               err = controlErr
+                       }
+                       return
+               },
+       }
+       if dialTcpFromListenPort {
+               netDialer.LocalAddr = l.Addr()
+       }
+       s = tcpSocket{
                Listener: l,
                NetworkDialer: NetworkDialer{
                        Network: network,
-                       Dialer:  DefaultNetDialer,
+                       Dialer:  &netDialer,
                },
-       }, err
+       }
+       return
 }
 
 type tcpSocket struct {
@@ -51,7 +111,7 @@ type tcpSocket struct {
        NetworkDialer
 }
 
-func listenAll(networks []network, getHost func(string) string, port int, f firewallCallback) ([]socket, error) {
+func listenAll(networks []network, getHost func(string) string, port int, f firewallCallback, logger log.Logger) ([]socket, error) {
        if len(networks) == 0 {
                return nil, nil
        }
@@ -60,7 +120,7 @@ func listenAll(networks []network, getHost func(string) string, port int, f fire
                nahs = append(nahs, networkAndHost{n, getHost(n.String())})
        }
        for {
-               ss, retry, err := listenAllRetry(nahs, port, f)
+               ss, retry, err := listenAllRetry(nahs, port, f, logger)
                if !retry {
                        return ss, err
                }
@@ -72,10 +132,10 @@ type networkAndHost struct {
        Host    string
 }
 
-func listenAllRetry(nahs []networkAndHost, port int, f firewallCallback) (ss []socket, retry bool, err error) {
+func listenAllRetry(nahs []networkAndHost, port int, f firewallCallback, logger log.Logger) (ss []socket, retry bool, err error) {
        ss = make([]socket, 1, len(nahs))
        portStr := strconv.FormatInt(int64(port), 10)
-       ss[0], err = listen(nahs[0].Network, net.JoinHostPort(nahs[0].Host, portStr), f)
+       ss[0], err = listen(nahs[0].Network, net.JoinHostPort(nahs[0].Host, portStr), f, logger)
        if err != nil {
                return nil, false, errors.Wrap(err, "first listen")
        }
@@ -89,7 +149,7 @@ func listenAllRetry(nahs []networkAndHost, port int, f firewallCallback) (ss []s
        }()
        portStr = strconv.FormatInt(int64(missinggo.AddrPort(ss[0].Addr())), 10)
        for _, nah := range nahs[1:] {
-               s, err := listen(nah.Network, net.JoinHostPort(nah.Host, portStr), f)
+               s, err := listen(nah.Network, net.JoinHostPort(nah.Host, portStr), f, logger)
                if err != nil {
                        return ss,
                                missinggo.IsAddrInUse(err) && port == 0,
@@ -100,10 +160,11 @@ func listenAllRetry(nahs []networkAndHost, port int, f firewallCallback) (ss []s
        return
 }
 
+// This isn't aliased from go-libutp since that assumes CGO.
 type firewallCallback func(net.Addr) bool
 
-func listenUtp(network, addr string, fc firewallCallback) (socket, error) {
-       us, err := NewUtpSocket(network, addr, fc)
+func listenUtp(network, addr string, fc firewallCallback, logger log.Logger) (socket, error) {
+       us, err := NewUtpSocket(network, addr, fc, logger)
        return utpSocketSocket{us, network}, err
 }