package p2p import ( "math/rand" "net" "sync" "time" ) const ( // FuzzModeDrop is a mode in which we randomly drop reads/writes, connections or sleep FuzzModeDrop = iota // FuzzModeDelay is a mode in which we randomly sleep FuzzModeDelay ) // FuzzedConnection wraps any net.Conn and depending on the mode either delays // reads/writes or randomly drops reads/writes/connections. type FuzzedConnection struct { conn net.Conn mtx sync.Mutex start <-chan time.Time active bool config *FuzzConnConfig } // FuzzConnConfig is a FuzzedConnection configuration. type FuzzConnConfig struct { Mode int MaxDelay time.Duration ProbDropRW float64 ProbDropConn float64 ProbSleep float64 } // DefaultFuzzConnConfig returns the default config. func DefaultFuzzConnConfig() *FuzzConnConfig { return &FuzzConnConfig{ Mode: FuzzModeDrop, MaxDelay: 3 * time.Second, ProbDropRW: 0.2, ProbDropConn: 0.00, ProbSleep: 0.00, } } // FuzzConn creates a new FuzzedConnection. Fuzzing starts immediately. func FuzzConn(conn net.Conn) net.Conn { return FuzzConnFromConfig(conn, DefaultFuzzConnConfig()) } // FuzzConnFromConfig creates a new FuzzedConnection from a config. Fuzzing // starts immediately. func FuzzConnFromConfig(conn net.Conn, config *FuzzConnConfig) net.Conn { return &FuzzedConnection{ conn: conn, start: make(<-chan time.Time), active: true, config: config, } } // FuzzConnAfter creates a new FuzzedConnection. Fuzzing starts when the // duration elapses. func FuzzConnAfter(conn net.Conn, d time.Duration) net.Conn { return FuzzConnAfterFromConfig(conn, d, DefaultFuzzConnConfig()) } // FuzzConnAfterFromConfig creates a new FuzzedConnection from a config. // Fuzzing starts when the duration elapses. func FuzzConnAfterFromConfig(conn net.Conn, d time.Duration, config *FuzzConnConfig) net.Conn { return &FuzzedConnection{ conn: conn, start: time.After(d), active: false, config: config, } } // Config returns the connection's config. func (fc *FuzzedConnection) Config() *FuzzConnConfig { return fc.config } // Read implements net.Conn. func (fc *FuzzedConnection) Read(data []byte) (n int, err error) { if fc.fuzz() { return 0, nil } return fc.conn.Read(data) } // Write implements net.Conn. func (fc *FuzzedConnection) Write(data []byte) (n int, err error) { if fc.fuzz() { return 0, nil } return fc.conn.Write(data) } // Close implements net.Conn. func (fc *FuzzedConnection) Close() error { return fc.conn.Close() } // LocalAddr implements net.Conn. func (fc *FuzzedConnection) LocalAddr() net.Addr { return fc.conn.LocalAddr() } // RemoteAddr implements net.Conn. func (fc *FuzzedConnection) RemoteAddr() net.Addr { return fc.conn.RemoteAddr() } // SetDeadline implements net.Conn. func (fc *FuzzedConnection) SetDeadline(t time.Time) error { return fc.conn.SetDeadline(t) } // SetReadDeadline implements net.Conn. func (fc *FuzzedConnection) SetReadDeadline(t time.Time) error { return fc.conn.SetReadDeadline(t) } // SetWriteDeadline implements net.Conn. func (fc *FuzzedConnection) SetWriteDeadline(t time.Time) error { return fc.conn.SetWriteDeadline(t) } func (fc *FuzzedConnection) randomDuration() time.Duration { maxDelayMillis := int(fc.config.MaxDelay.Nanoseconds() / 1000) return time.Millisecond * time.Duration(rand.Int()%maxDelayMillis) // nolint: gas } // implements the fuzz (delay, kill conn) // and returns whether or not the read/write should be ignored func (fc *FuzzedConnection) fuzz() bool { if !fc.shouldFuzz() { return false } switch fc.config.Mode { case FuzzModeDrop: // randomly drop the r/w, drop the conn, or sleep r := rand.Float64() if r <= fc.config.ProbDropRW { return true } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn { // XXX: can't this fail because machine precision? // XXX: do we need an error? fc.Close() // nolint: errcheck, gas return true } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep { time.Sleep(fc.randomDuration()) } case FuzzModeDelay: // sleep a bit time.Sleep(fc.randomDuration()) } return false } func (fc *FuzzedConnection) shouldFuzz() bool { if fc.active { return true } fc.mtx.Lock() defer fc.mtx.Unlock() select { case <-fc.start: fc.active = true return true default: return false } }