github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/activity.go (about) 1 /* 2 * Copyright (c) 2020, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package common 21 22 import ( 23 "net" 24 "sync/atomic" 25 "time" 26 27 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" 28 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/monotime" 29 ) 30 31 // ActivityMonitoredConn wraps a net.Conn, adding logic to deal with events 32 // triggered by I/O activity. 33 // 34 // ActivityMonitoredConn uses lock-free concurrency synronization, avoiding an 35 // additional mutex resource, making it suitable for wrapping many net.Conns 36 // (e.g, each Psiphon port forward). 37 // 38 // When an inactivity timeout is specified, the network I/O will timeout after 39 // the specified period of read inactivity. Optionally, for the purpose of 40 // inactivity only, ActivityMonitoredConn will also consider the connection 41 // active when data is written to it. 42 // 43 // When a LRUConnsEntry is specified, then the LRU entry is promoted on either 44 // a successful read or write. 45 // 46 // When an ActivityUpdater is set, then its UpdateActivity method is called on 47 // each read and write with the number of bytes transferred. The 48 // durationNanoseconds, which is the time since the last read, is reported 49 // only on reads. 50 type ActivityMonitoredConn struct { 51 // Note: 64-bit ints used with atomic operations are placed 52 // at the start of struct to ensure 64-bit alignment. 53 // (https://golang.org/pkg/sync/atomic/#pkg-note-BUG) 54 monotonicStartTime int64 55 lastReadActivityTime int64 56 realStartTime time.Time 57 net.Conn 58 inactivityTimeout time.Duration 59 activeOnWrite bool 60 activityUpdaters []ActivityUpdater 61 lruEntry *LRUConnsEntry 62 } 63 64 // ActivityUpdater defines an interface for receiving updates for 65 // ActivityMonitoredConn activity. Values passed to UpdateProgress are bytes 66 // transferred and conn duration since the previous UpdateProgress. 67 type ActivityUpdater interface { 68 UpdateProgress(bytesRead, bytesWritten, durationNanoseconds int64) 69 } 70 71 // NewActivityMonitoredConn creates a new ActivityMonitoredConn. 72 func NewActivityMonitoredConn( 73 conn net.Conn, 74 inactivityTimeout time.Duration, 75 activeOnWrite bool, 76 lruEntry *LRUConnsEntry, 77 activityUpdaters ...ActivityUpdater) (*ActivityMonitoredConn, error) { 78 79 if inactivityTimeout > 0 { 80 err := conn.SetDeadline(time.Now().Add(inactivityTimeout)) 81 if err != nil { 82 return nil, errors.Trace(err) 83 } 84 } 85 86 // The "monotime" package is still used here as its time value is an int64, 87 // which is compatible with atomic operations. 88 89 now := int64(monotime.Now()) 90 91 return &ActivityMonitoredConn{ 92 Conn: conn, 93 inactivityTimeout: inactivityTimeout, 94 activeOnWrite: activeOnWrite, 95 realStartTime: time.Now(), 96 monotonicStartTime: now, 97 lastReadActivityTime: now, 98 lruEntry: lruEntry, 99 activityUpdaters: activityUpdaters, 100 }, nil 101 } 102 103 // GetStartTime gets the time when the ActivityMonitoredConn was initialized. 104 // Reported time is UTC. 105 func (conn *ActivityMonitoredConn) GetStartTime() time.Time { 106 return conn.realStartTime.UTC() 107 } 108 109 // GetActiveDuration returns the time elapsed between the initialization of 110 // the ActivityMonitoredConn and the last Read. Only reads are used for this 111 // calculation since writes may succeed locally due to buffering. 112 func (conn *ActivityMonitoredConn) GetActiveDuration() time.Duration { 113 return time.Duration(atomic.LoadInt64(&conn.lastReadActivityTime) - conn.monotonicStartTime) 114 } 115 116 func (conn *ActivityMonitoredConn) Read(buffer []byte) (int, error) { 117 n, err := conn.Conn.Read(buffer) 118 if n > 0 { 119 120 if conn.inactivityTimeout > 0 { 121 err = conn.Conn.SetDeadline(time.Now().Add(conn.inactivityTimeout)) 122 if err != nil { 123 return n, errors.Trace(err) 124 } 125 } 126 127 lastReadActivityTime := atomic.LoadInt64(&conn.lastReadActivityTime) 128 readActivityTime := int64(monotime.Now()) 129 130 atomic.StoreInt64(&conn.lastReadActivityTime, readActivityTime) 131 132 for _, activityUpdater := range conn.activityUpdaters { 133 activityUpdater.UpdateProgress( 134 int64(n), 0, readActivityTime-lastReadActivityTime) 135 } 136 137 if conn.lruEntry != nil { 138 conn.lruEntry.Touch() 139 } 140 } 141 // Note: no trace error to preserve error type 142 return n, err 143 } 144 145 func (conn *ActivityMonitoredConn) Write(buffer []byte) (int, error) { 146 n, err := conn.Conn.Write(buffer) 147 if n > 0 { 148 149 // Bytes written are reported regardless of activeOnWrite. Inactivity 150 // deadline extension and LRU updates are conditional on activeOnWrite. 151 152 for _, activityUpdater := range conn.activityUpdaters { 153 activityUpdater.UpdateProgress(0, int64(n), 0) 154 } 155 156 if conn.activeOnWrite { 157 158 if conn.inactivityTimeout > 0 { 159 err = conn.Conn.SetDeadline(time.Now().Add(conn.inactivityTimeout)) 160 if err != nil { 161 return n, errors.Trace(err) 162 } 163 } 164 165 if conn.lruEntry != nil { 166 conn.lruEntry.Touch() 167 } 168 } 169 } 170 // Note: no trace error to preserve error type 171 return n, err 172 } 173 174 // IsClosed implements the Closer iterface. The return value indicates whether 175 // the underlying conn has been closed. 176 func (conn *ActivityMonitoredConn) IsClosed() bool { 177 closer, ok := conn.Conn.(Closer) 178 if !ok { 179 return false 180 } 181 return closer.IsClosed() 182 }