mirror of
https://github.com/netbirdio/ice.git
synced 2026-05-22 17:10:58 -07:00
Make Candidate an interface
This change will allow us to have custom logic and members per interface type. Relay candidates will have a completely different read loop, and candidate specific state. Relates to #47
This commit is contained in:
@@ -54,7 +54,7 @@ type bindingRequest struct {
|
||||
// Agent represents the ICE agent
|
||||
type Agent struct {
|
||||
onConnectionStateChangeHdlr func(ConnectionState)
|
||||
onSelectedCandidatePairChangeHdlr func(*Candidate, *Candidate)
|
||||
onSelectedCandidatePairChangeHdlr func(Candidate, Candidate)
|
||||
|
||||
// Used to block double Dial/Accept
|
||||
opened bool
|
||||
@@ -91,15 +91,15 @@ type Agent struct {
|
||||
|
||||
localUfrag string
|
||||
localPwd string
|
||||
localCandidates map[NetworkType][]*Candidate
|
||||
localCandidates map[NetworkType][]Candidate
|
||||
|
||||
remoteUfrag string
|
||||
remotePwd string
|
||||
remoteCandidates map[NetworkType][]*Candidate
|
||||
remoteCandidates map[NetworkType][]Candidate
|
||||
|
||||
selector pairCandidateSelector
|
||||
selectedPair *candidatePair
|
||||
validPairs []*candidatePair
|
||||
validPairs candidatePairs
|
||||
|
||||
buffer *packetio.Buffer
|
||||
|
||||
@@ -175,8 +175,8 @@ func NewAgent(config *AgentConfig) (*Agent, error) {
|
||||
tieBreaker: rand.New(rand.NewSource(time.Now().UnixNano())).Uint64(),
|
||||
gatheringState: GatheringStateComplete, // TODO trickle-ice
|
||||
connectionState: ConnectionStateNew,
|
||||
localCandidates: make(map[NetworkType][]*Candidate),
|
||||
remoteCandidates: make(map[NetworkType][]*Candidate),
|
||||
localCandidates: make(map[NetworkType][]Candidate),
|
||||
remoteCandidates: make(map[NetworkType][]Candidate),
|
||||
pendingBindingRequests: make([]bindingRequest, 0, maxPendingBindingRequests),
|
||||
|
||||
localUfrag: randSeq(16),
|
||||
@@ -235,7 +235,7 @@ func (a *Agent) OnConnectionStateChange(f func(ConnectionState)) error {
|
||||
|
||||
// OnSelectedCandidatePairChange sets a handler that is fired when the final candidate
|
||||
// pair is selected
|
||||
func (a *Agent) OnSelectedCandidatePairChange(f func(*Candidate, *Candidate)) error {
|
||||
func (a *Agent) OnSelectedCandidatePairChange(f func(Candidate, Candidate)) error {
|
||||
return a.run(func(agent *Agent) {
|
||||
agent.onSelectedCandidatePairChangeHdlr = f
|
||||
})
|
||||
@@ -296,7 +296,7 @@ func (a *Agent) updateConnectionState(newState ConnectionState) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) findValidPair(local, remote *Candidate) *candidatePair {
|
||||
func (a *Agent) findValidPair(local, remote Candidate) *candidatePair {
|
||||
for _, p := range a.validPairs {
|
||||
if p.local == local && p.remote == remote {
|
||||
return p
|
||||
@@ -305,7 +305,7 @@ func (a *Agent) findValidPair(local, remote *Candidate) *candidatePair {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) addValidPair(local, remote *Candidate) *candidatePair {
|
||||
func (a *Agent) addValidPair(local, remote Candidate) *candidatePair {
|
||||
p := a.findValidPair(local, remote)
|
||||
if p != nil {
|
||||
a.log.Tracef("Candidate pair is already valid: %s", p)
|
||||
@@ -436,16 +436,15 @@ func (a *Agent) pingAllCandidates() {
|
||||
}
|
||||
|
||||
// AddRemoteCandidate adds a new remote candidate
|
||||
func (a *Agent) AddRemoteCandidate(c *Candidate) error {
|
||||
func (a *Agent) AddRemoteCandidate(c Candidate) error {
|
||||
return a.run(func(agent *Agent) {
|
||||
agent.addRemoteCandidate(c)
|
||||
})
|
||||
}
|
||||
|
||||
// addRemoteCandidate assumes you are holding the lock (must be execute using a.run)
|
||||
func (a *Agent) addRemoteCandidate(c *Candidate) {
|
||||
networkType := c.NetworkType
|
||||
set := a.remoteCandidates[networkType]
|
||||
func (a *Agent) addRemoteCandidate(c Candidate) {
|
||||
set := a.remoteCandidates[c.NetworkType()]
|
||||
|
||||
for _, candidate := range set {
|
||||
if candidate.Equal(c) {
|
||||
@@ -454,15 +453,15 @@ func (a *Agent) addRemoteCandidate(c *Candidate) {
|
||||
}
|
||||
|
||||
set = append(set, c)
|
||||
a.remoteCandidates[networkType] = set
|
||||
a.remoteCandidates[c.NetworkType()] = set
|
||||
}
|
||||
|
||||
// GetLocalCandidates returns the local candidates
|
||||
func (a *Agent) GetLocalCandidates() ([]*Candidate, error) {
|
||||
res := make(chan []*Candidate)
|
||||
func (a *Agent) GetLocalCandidates() ([]Candidate, error) {
|
||||
res := make(chan []Candidate)
|
||||
|
||||
err := a.run(func(agent *Agent) {
|
||||
var candidates []*Candidate
|
||||
var candidates []Candidate
|
||||
for _, set := range agent.localCandidates {
|
||||
candidates = append(candidates, set...)
|
||||
}
|
||||
@@ -525,7 +524,7 @@ func (a *Agent) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) findRemoteCandidate(networkType NetworkType, addr net.Addr) *Candidate {
|
||||
func (a *Agent) findRemoteCandidate(networkType NetworkType, addr net.Addr) Candidate {
|
||||
var ip net.IP
|
||||
var port int
|
||||
|
||||
@@ -543,16 +542,14 @@ func (a *Agent) findRemoteCandidate(networkType NetworkType, addr net.Addr) *Can
|
||||
|
||||
set := a.remoteCandidates[networkType]
|
||||
for _, c := range set {
|
||||
base := c
|
||||
if base.IP.Equal(ip) &&
|
||||
base.Port == port {
|
||||
if c.IP().Equal(ip) && c.Port() == port {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) sendBindingRequest(m *stun.Message, local, remote *Candidate) {
|
||||
func (a *Agent) sendBindingRequest(m *stun.Message, local, remote Candidate) {
|
||||
a.log.Tracef("ping STUN from %s to %s\n", local.String(), remote.String())
|
||||
|
||||
if overflow := len(a.pendingBindingRequests) - (maxPendingBindingRequests - 1); overflow > 0 {
|
||||
@@ -571,12 +568,12 @@ func (a *Agent) sendBindingRequest(m *stun.Message, local, remote *Candidate) {
|
||||
a.sendSTUN(m, local, remote)
|
||||
}
|
||||
|
||||
func (a *Agent) sendBindingSuccess(m *stun.Message, local, remote *Candidate) {
|
||||
func (a *Agent) sendBindingSuccess(m *stun.Message, local, remote Candidate) {
|
||||
base := remote
|
||||
if out, err := stun.Build(m, stun.BindingSuccess,
|
||||
&stun.XORMappedAddress{
|
||||
IP: base.IP,
|
||||
Port: base.Port,
|
||||
IP: base.IP(),
|
||||
Port: base.Port(),
|
||||
},
|
||||
stun.NewShortTermIntegrity(a.localPwd),
|
||||
stun.Fingerprint,
|
||||
@@ -601,7 +598,7 @@ func (a *Agent) handleInboundBindingSuccess(id [stun.TransactionIDSize]byte) (bo
|
||||
}
|
||||
|
||||
// handleInbound processes STUN traffic from a remote candidate
|
||||
func (a *Agent) handleInbound(m *stun.Message, local *Candidate, remote net.Addr) {
|
||||
func (a *Agent) handleInbound(m *stun.Message, local Candidate, remote net.Addr) {
|
||||
var err error
|
||||
if m == nil || local == nil {
|
||||
return
|
||||
@@ -630,7 +627,7 @@ func (a *Agent) handleInbound(m *stun.Message, local *Candidate, remote net.Addr
|
||||
}
|
||||
}
|
||||
|
||||
remoteCandidate := a.findRemoteCandidate(local.NetworkType, remote)
|
||||
remoteCandidate := a.findRemoteCandidate(local.NetworkType(), remote)
|
||||
if m.Type.Class == stun.ClassSuccessResponse {
|
||||
if err = assertInboundMessageIntegrity(m, []byte(a.remotePwd)); err != nil {
|
||||
a.log.Warnf("discard message from (%s), %v", remote, err)
|
||||
@@ -659,7 +656,7 @@ func (a *Agent) handleInbound(m *stun.Message, local *Candidate, remote net.Addr
|
||||
return
|
||||
}
|
||||
|
||||
prflxCandidate, err := NewCandidatePeerReflexive(networkType.String(), ip, port, local.Component, "", 0)
|
||||
prflxCandidate, err := NewCandidatePeerReflexive(networkType.String(), ip, port, local.Component(), "", 0)
|
||||
if err != nil {
|
||||
a.log.Errorf("Failed to create new remote prflx candidate (%s)", err)
|
||||
return
|
||||
@@ -682,8 +679,8 @@ func (a *Agent) handleInbound(m *stun.Message, local *Candidate, remote net.Addr
|
||||
|
||||
// noSTUNSeen processes non STUN traffic from a remote candidate,
|
||||
// and returns true if it is an actual remote candidate
|
||||
func (a *Agent) noSTUNSeen(local *Candidate, remote net.Addr) bool {
|
||||
remoteCandidate := a.findRemoteCandidate(local.NetworkType, remote)
|
||||
func (a *Agent) noSTUNSeen(local Candidate, remote net.Addr) bool {
|
||||
remoteCandidate := a.findRemoteCandidate(local.NetworkType(), remote)
|
||||
if remoteCandidate == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
+6
-6
@@ -108,7 +108,7 @@ func TestPairPriority(t *testing.T) {
|
||||
t.Fatalf("Failed to construct remote host candidate: %s", err)
|
||||
}
|
||||
|
||||
for _, remote := range []*Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote} {
|
||||
for _, remote := range []Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote} {
|
||||
a.addValidPair(hostLocal, remote)
|
||||
bestPair := a.getBestValidPair()
|
||||
if bestPair.String() != (&candidatePair{remote: remote, local: hostLocal}).String() {
|
||||
@@ -130,7 +130,7 @@ func TestOnSelectedCandidatePairChange(t *testing.T) {
|
||||
t.Fatalf("Failed to create agent: %s", err)
|
||||
}
|
||||
callbackCalled := make(chan struct{}, 1)
|
||||
if err = a.OnSelectedCandidatePairChange(func(local, remote *Candidate) {
|
||||
if err = a.OnSelectedCandidatePairChange(func(local, remote Candidate) {
|
||||
close(callbackCalled)
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to set agent OnCandidatePairChange callback: %s", err)
|
||||
@@ -226,22 +226,22 @@ func TestHandlePeerReflexive(t *testing.T) {
|
||||
}
|
||||
|
||||
// length of remote candidate list for a network type must be 1
|
||||
set := a.remoteCandidates[local.NetworkType]
|
||||
set := a.remoteCandidates[local.NetworkType()]
|
||||
if len(set) != 1 {
|
||||
t.Fatal("failed to add prflx candidate to remote candidate list")
|
||||
}
|
||||
|
||||
c := set[0]
|
||||
|
||||
if c.Type != CandidateTypePeerReflexive {
|
||||
if c.Type() != CandidateTypePeerReflexive {
|
||||
t.Fatal("candidate type must be prflx")
|
||||
}
|
||||
|
||||
if !c.IP.Equal(net.ParseIP("172.17.0.3")) {
|
||||
if !c.IP().Equal(net.ParseIP("172.17.0.3")) {
|
||||
t.Fatal("IP address mismatch")
|
||||
}
|
||||
|
||||
if c.Port != 999 {
|
||||
if c.Port() != 999 {
|
||||
t.Fatal("Port number mismatch")
|
||||
}
|
||||
|
||||
|
||||
+19
-257
@@ -1,12 +1,8 @@
|
||||
package ice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,260 +16,26 @@ const (
|
||||
)
|
||||
|
||||
// Candidate represents an ICE candidate
|
||||
type Candidate struct {
|
||||
NetworkType
|
||||
type Candidate interface {
|
||||
start(a *Agent, conn net.PacketConn)
|
||||
addr() net.Addr
|
||||
|
||||
Type CandidateType
|
||||
LocalPreference uint16
|
||||
Component uint16
|
||||
IP net.IP
|
||||
Port int
|
||||
RelatedAddress *CandidateRelatedAddress
|
||||
setLastSent(t time.Time)
|
||||
seen(outbound bool)
|
||||
LastSent() time.Time
|
||||
setLastReceived(t time.Time)
|
||||
LastReceived() time.Time
|
||||
String() string
|
||||
Equal(other Candidate) bool
|
||||
Priority() uint32
|
||||
writeTo(raw []byte, dst Candidate) (int, error)
|
||||
close() error
|
||||
|
||||
lock sync.RWMutex
|
||||
lastSent time.Time
|
||||
lastReceived time.Time
|
||||
IP() net.IP
|
||||
Port() int
|
||||
Component() uint16
|
||||
NetworkType() NetworkType
|
||||
|
||||
agent *Agent
|
||||
conn net.PacketConn
|
||||
closeCh chan struct{}
|
||||
closedCh chan struct{}
|
||||
}
|
||||
|
||||
// NewCandidateHost creates a new host candidate
|
||||
func NewCandidateHost(network string, ip net.IP, port int, component uint16) (*Candidate, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Candidate{
|
||||
Type: CandidateTypeHost,
|
||||
NetworkType: networkType,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: component,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCandidateServerReflexive creates a new server reflective candidate
|
||||
func NewCandidateServerReflexive(network string, ip net.IP, port int, component uint16, relAddr string, relPort int) (*Candidate, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Candidate{
|
||||
Type: CandidateTypeServerReflexive,
|
||||
NetworkType: networkType,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: component,
|
||||
RelatedAddress: &CandidateRelatedAddress{
|
||||
Address: relAddr,
|
||||
Port: relPort,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCandidatePeerReflexive creates a new peer reflective candidate
|
||||
func NewCandidatePeerReflexive(network string, ip net.IP, port int, component uint16, relAddr string, relPort int) (*Candidate, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Candidate{
|
||||
Type: CandidateTypePeerReflexive,
|
||||
NetworkType: networkType,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: component,
|
||||
RelatedAddress: &CandidateRelatedAddress{
|
||||
Address: relAddr,
|
||||
Port: relPort,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCandidateRelay creates a new relay candidate
|
||||
func NewCandidateRelay(network string, ip net.IP, port int, component uint16, relAddr string, relPort int) (*Candidate, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Candidate{
|
||||
Type: CandidateTypeRelay,
|
||||
NetworkType: networkType,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: component,
|
||||
RelatedAddress: &CandidateRelatedAddress{
|
||||
Address: relAddr,
|
||||
Port: relPort,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// start runs the candidate using the provided connection
|
||||
func (c *Candidate) start(a *Agent, conn net.PacketConn) {
|
||||
c.agent = a
|
||||
c.conn = conn
|
||||
c.closeCh = make(chan struct{})
|
||||
c.closedCh = make(chan struct{})
|
||||
|
||||
go c.recvLoop()
|
||||
}
|
||||
|
||||
func (c *Candidate) recvLoop() {
|
||||
defer func() {
|
||||
close(c.closedCh)
|
||||
}()
|
||||
|
||||
log := c.agent.log
|
||||
buffer := make([]byte, receiveMTU)
|
||||
for {
|
||||
n, srcAddr, err := c.conn.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if stun.IsMessage(buffer[:n]) {
|
||||
m := &stun.Message{
|
||||
Raw: make([]byte, n),
|
||||
}
|
||||
// Explicitly copy raw buffer so Message can own the memory.
|
||||
copy(m.Raw, buffer[:n])
|
||||
if err = m.Decode(); err != nil {
|
||||
log.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err)
|
||||
continue
|
||||
}
|
||||
err = c.agent.run(func(agent *Agent) {
|
||||
agent.handleInbound(m, c, srcAddr)
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("Failed to handle message: %v", err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
isValidRemoteCandidate := make(chan bool, 1)
|
||||
err = c.agent.run(func(agent *Agent) {
|
||||
isValidRemoteCandidate <- agent.noSTUNSeen(c, srcAddr)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to handle message: %v", err)
|
||||
} else if !<-isValidRemoteCandidate {
|
||||
log.Warnf("Discarded message from %s, not a valid remote candidate", c.addr())
|
||||
}
|
||||
|
||||
// NOTE This will return packetio.ErrFull if the buffer ever manages to fill up.
|
||||
_, err = c.agent.buffer.Write(buffer[:n])
|
||||
if err != nil {
|
||||
log.Warnf("failed to write packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// close stops the recvLoop
|
||||
func (c *Candidate) close() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.conn != nil {
|
||||
// Unblock recvLoop
|
||||
close(c.closeCh)
|
||||
// Close the conn
|
||||
err := c.conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait until the recvLoop is closed
|
||||
<-c.closedCh
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Candidate) writeTo(raw []byte, dst *Candidate) (int, error) {
|
||||
n, err := c.conn.WriteTo(raw, dst.addr())
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to send packet: %v", err)
|
||||
}
|
||||
c.seen(true)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Priority computes the priority for this ICE Candidate
|
||||
func (c *Candidate) Priority() uint32 {
|
||||
// The local preference MUST be an integer from 0 (lowest preference) to
|
||||
// 65535 (highest preference) inclusive. When there is only a single IP
|
||||
// address, this value SHOULD be set to 65535. If there are multiple
|
||||
// candidates for a particular component for a particular data stream
|
||||
// that have the same type, the local preference MUST be unique for each
|
||||
// one.
|
||||
return (1<<24)*uint32(c.Type.Preference()) +
|
||||
(1<<8)*uint32(c.LocalPreference) +
|
||||
uint32(256-c.Component)
|
||||
}
|
||||
|
||||
// Equal is used to compare two CandidateBases
|
||||
func (c *Candidate) Equal(other *Candidate) bool {
|
||||
return c.NetworkType == other.NetworkType &&
|
||||
c.Type == other.Type &&
|
||||
c.IP.Equal(other.IP) &&
|
||||
c.Port == other.Port &&
|
||||
c.RelatedAddress.Equal(other.RelatedAddress)
|
||||
}
|
||||
|
||||
// String makes the CandidateHost printable
|
||||
func (c *Candidate) String() string {
|
||||
return fmt.Sprintf("%s %s:%d%s", c.Type, c.IP, c.Port, c.RelatedAddress)
|
||||
}
|
||||
|
||||
// LastReceived returns a time.Time indicating the last time
|
||||
// this candidate was received
|
||||
func (c *Candidate) LastReceived() time.Time {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.lastReceived
|
||||
}
|
||||
|
||||
func (c *Candidate) setLastReceived(t time.Time) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lastReceived = t
|
||||
}
|
||||
|
||||
// LastSent returns a time.Time indicating the last time
|
||||
// this candidate was sent
|
||||
func (c *Candidate) LastSent() time.Time {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.lastSent
|
||||
}
|
||||
|
||||
func (c *Candidate) setLastSent(t time.Time) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lastSent = t
|
||||
}
|
||||
|
||||
func (c *Candidate) seen(outbound bool) {
|
||||
if outbound {
|
||||
c.setLastSent(time.Now())
|
||||
} else {
|
||||
c.setLastReceived(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Candidate) addr() net.Addr {
|
||||
return &net.UDPAddr{
|
||||
IP: c.IP,
|
||||
Port: c.Port,
|
||||
}
|
||||
Type() CandidateType
|
||||
RelatedAddress() *CandidateRelatedAddress
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package ice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
type candidateBase struct {
|
||||
networkType NetworkType
|
||||
candidateType CandidateType
|
||||
|
||||
component uint16
|
||||
ip net.IP
|
||||
port int
|
||||
relatedAddress *CandidateRelatedAddress
|
||||
|
||||
lock sync.RWMutex
|
||||
lastSent time.Time
|
||||
lastReceived time.Time
|
||||
|
||||
agent *Agent
|
||||
conn net.PacketConn
|
||||
closeCh chan struct{}
|
||||
closedCh chan struct{}
|
||||
}
|
||||
|
||||
// IP returns Candidate IP
|
||||
func (c *candidateBase) IP() net.IP {
|
||||
return c.ip
|
||||
}
|
||||
|
||||
// Port returns Candidate Port
|
||||
func (c *candidateBase) Port() int {
|
||||
return c.port
|
||||
}
|
||||
|
||||
// Type returns candidate type
|
||||
func (c *candidateBase) Type() CandidateType {
|
||||
return c.candidateType
|
||||
}
|
||||
|
||||
// NetworkType returns candidate NetworkType
|
||||
func (c *candidateBase) NetworkType() NetworkType {
|
||||
return c.networkType
|
||||
}
|
||||
|
||||
// Component returns candidate component
|
||||
func (c *candidateBase) Component() uint16 {
|
||||
return c.component
|
||||
}
|
||||
|
||||
// LocalPreference returns the local preference for this candidate
|
||||
func (c *candidateBase) LocalPreference() uint16 {
|
||||
return defaultLocalPreference
|
||||
}
|
||||
|
||||
// RelatedAddress returns *CandidateRelatedAddress
|
||||
func (c *candidateBase) RelatedAddress() *CandidateRelatedAddress {
|
||||
return c.relatedAddress
|
||||
}
|
||||
|
||||
// start runs the candidate using the provided connection
|
||||
func (c *candidateBase) start(a *Agent, conn net.PacketConn) {
|
||||
c.agent = a
|
||||
c.conn = conn
|
||||
c.closeCh = make(chan struct{})
|
||||
c.closedCh = make(chan struct{})
|
||||
|
||||
go c.recvLoop()
|
||||
}
|
||||
|
||||
func (c *candidateBase) recvLoop() {
|
||||
defer func() {
|
||||
close(c.closedCh)
|
||||
}()
|
||||
|
||||
log := c.agent.log
|
||||
buffer := make([]byte, receiveMTU)
|
||||
for {
|
||||
n, srcAddr, err := c.conn.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if stun.IsMessage(buffer[:n]) {
|
||||
m := &stun.Message{
|
||||
Raw: make([]byte, n),
|
||||
}
|
||||
// Explicitly copy raw buffer so Message can own the memory.
|
||||
copy(m.Raw, buffer[:n])
|
||||
if err = m.Decode(); err != nil {
|
||||
log.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err)
|
||||
continue
|
||||
}
|
||||
err = c.agent.run(func(agent *Agent) {
|
||||
agent.handleInbound(m, c, srcAddr)
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("Failed to handle message: %v", err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
isValidRemoteCandidate := make(chan bool, 1)
|
||||
err = c.agent.run(func(agent *Agent) {
|
||||
isValidRemoteCandidate <- agent.noSTUNSeen(c, srcAddr)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to handle message: %v", err)
|
||||
} else if !<-isValidRemoteCandidate {
|
||||
log.Warnf("Discarded message from %s, not a valid remote candidate", c.addr())
|
||||
}
|
||||
|
||||
// NOTE This will return packetio.ErrFull if the buffer ever manages to fill up.
|
||||
_, err = c.agent.buffer.Write(buffer[:n])
|
||||
if err != nil {
|
||||
log.Warnf("failed to write packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// close stops the recvLoop
|
||||
func (c *candidateBase) close() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.conn != nil {
|
||||
// Unblock recvLoop
|
||||
close(c.closeCh)
|
||||
// Close the conn
|
||||
err := c.conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait until the recvLoop is closed
|
||||
<-c.closedCh
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *candidateBase) writeTo(raw []byte, dst Candidate) (int, error) {
|
||||
n, err := c.conn.WriteTo(raw, dst.addr())
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to send packet: %v", err)
|
||||
}
|
||||
c.seen(true)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Priority computes the priority for this ICE Candidate
|
||||
func (c *candidateBase) Priority() uint32 {
|
||||
// The local preference MUST be an integer from 0 (lowest preference) to
|
||||
// 65535 (highest preference) inclusive. When there is only a single IP
|
||||
// address, this value SHOULD be set to 65535. If there are multiple
|
||||
// candidates for a particular component for a particular data stream
|
||||
// that have the same type, the local preference MUST be unique for each
|
||||
// one.
|
||||
return (1<<24)*uint32(c.Type().Preference()) +
|
||||
(1<<8)*uint32(c.LocalPreference()) +
|
||||
uint32(256-c.Component())
|
||||
}
|
||||
|
||||
// Equal is used to compare two candidateBases
|
||||
func (c *candidateBase) Equal(other Candidate) bool {
|
||||
return c.NetworkType() == other.NetworkType() &&
|
||||
c.Type() == other.Type() &&
|
||||
c.IP().Equal(other.IP()) &&
|
||||
c.Port() == other.Port() &&
|
||||
c.RelatedAddress().Equal(other.RelatedAddress())
|
||||
}
|
||||
|
||||
// String makes the candidateBase printable
|
||||
func (c *candidateBase) String() string {
|
||||
return fmt.Sprintf("%s %s:%d%s", c.Type(), c.IP(), c.Port(), c.relatedAddress)
|
||||
}
|
||||
|
||||
// LastReceived returns a time.Time indicating the last time
|
||||
// this candidate was received
|
||||
func (c *candidateBase) LastReceived() time.Time {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.lastReceived
|
||||
}
|
||||
|
||||
func (c *candidateBase) setLastReceived(t time.Time) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lastReceived = t
|
||||
}
|
||||
|
||||
// LastSent returns a time.Time indicating the last time
|
||||
// this candidate was sent
|
||||
func (c *candidateBase) LastSent() time.Time {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.lastSent
|
||||
}
|
||||
|
||||
func (c *candidateBase) setLastSent(t time.Time) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lastSent = t
|
||||
}
|
||||
|
||||
func (c *candidateBase) seen(outbound bool) {
|
||||
if outbound {
|
||||
c.setLastSent(time.Now())
|
||||
} else {
|
||||
c.setLastReceived(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *candidateBase) addr() net.Addr {
|
||||
return &net.UDPAddr{
|
||||
IP: c.IP(),
|
||||
Port: c.Port(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ice
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// CandidateHost is a candidate of type host
|
||||
type CandidateHost struct {
|
||||
candidateBase
|
||||
}
|
||||
|
||||
// NewCandidateHost creates a new host candidate
|
||||
func NewCandidateHost(network string, ip net.IP, port int, component uint16) (*CandidateHost, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CandidateHost{
|
||||
candidateBase: candidateBase{
|
||||
networkType: networkType,
|
||||
candidateType: CandidateTypeHost,
|
||||
ip: ip,
|
||||
port: port,
|
||||
component: component,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package ice
|
||||
|
||||
import "net"
|
||||
|
||||
// CandidatePeerReflexive ...
|
||||
type CandidatePeerReflexive struct {
|
||||
candidateBase
|
||||
}
|
||||
|
||||
// NewCandidatePeerReflexive creates a new peer reflective candidate
|
||||
func NewCandidatePeerReflexive(network string, ip net.IP, port int, component uint16, relAddr string, relPort int) (*CandidatePeerReflexive, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CandidatePeerReflexive{
|
||||
candidateBase: candidateBase{
|
||||
networkType: networkType,
|
||||
candidateType: CandidateTypePeerReflexive,
|
||||
ip: ip,
|
||||
port: port,
|
||||
component: component,
|
||||
relatedAddress: &CandidateRelatedAddress{
|
||||
Address: relAddr,
|
||||
Port: relPort,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package ice
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// CandidateRelay ...
|
||||
type CandidateRelay struct {
|
||||
candidateBase
|
||||
}
|
||||
|
||||
// NewCandidateRelay creates a new relay candidate
|
||||
func NewCandidateRelay(network string, ip net.IP, port int, component uint16, relAddr string, relPort int) (*CandidateRelay, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CandidateRelay{
|
||||
candidateBase: candidateBase{
|
||||
networkType: networkType,
|
||||
candidateType: CandidateTypeRelay,
|
||||
ip: ip,
|
||||
port: port,
|
||||
component: component,
|
||||
relatedAddress: &CandidateRelatedAddress{
|
||||
Address: relAddr,
|
||||
Port: relPort,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package ice
|
||||
|
||||
import "net"
|
||||
|
||||
// CandidateServerReflexive ...
|
||||
type CandidateServerReflexive struct {
|
||||
candidateBase
|
||||
}
|
||||
|
||||
// NewCandidateServerReflexive creates a new server reflective candidate
|
||||
func NewCandidateServerReflexive(network string, ip net.IP, port int, component uint16, relAddr string, relPort int) (*CandidateServerReflexive, error) {
|
||||
networkType, err := determineNetworkType(network, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CandidateServerReflexive{
|
||||
candidateBase: candidateBase{
|
||||
networkType: networkType,
|
||||
candidateType: CandidateTypeServerReflexive,
|
||||
ip: ip,
|
||||
port: port,
|
||||
component: component,
|
||||
relatedAddress: &CandidateRelatedAddress{
|
||||
Address: relAddr,
|
||||
Port: relPort,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
+21
-17
@@ -4,38 +4,42 @@ import "testing"
|
||||
|
||||
func TestCandidatePriority(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
Candidate *Candidate
|
||||
Candidate Candidate
|
||||
WantPriority uint32
|
||||
}{
|
||||
{
|
||||
Candidate: &Candidate{
|
||||
Type: CandidateTypeHost,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
Candidate: &CandidateHost{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypeHost,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
},
|
||||
WantPriority: 2130706431,
|
||||
},
|
||||
{
|
||||
Candidate: &Candidate{
|
||||
Type: CandidateTypePeerReflexive,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
Candidate: &CandidatePeerReflexive{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypePeerReflexive,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
},
|
||||
WantPriority: 1862270975,
|
||||
},
|
||||
{
|
||||
Candidate: &Candidate{
|
||||
Type: CandidateTypeServerReflexive,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
Candidate: &CandidateServerReflexive{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypeServerReflexive,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
},
|
||||
WantPriority: 1694498815,
|
||||
},
|
||||
{
|
||||
Candidate: &Candidate{
|
||||
Type: CandidateTypeRelay,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
Candidate: &CandidateRelay{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypeRelay,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
},
|
||||
WantPriority: 16777215,
|
||||
},
|
||||
|
||||
+5
-5
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
func newCandidatePair(local, remote *Candidate, controlling bool) *candidatePair {
|
||||
func newCandidatePair(local, remote Candidate, controlling bool) *candidatePair {
|
||||
return &candidatePair{
|
||||
iceRoleControlling: controlling,
|
||||
remote: remote,
|
||||
@@ -17,8 +17,8 @@ func newCandidatePair(local, remote *Candidate, controlling bool) *candidatePair
|
||||
// candidatePair represents a combination of a local and remote candidate
|
||||
type candidatePair struct {
|
||||
iceRoleControlling bool
|
||||
remote *Candidate
|
||||
local *Candidate
|
||||
remote Candidate
|
||||
local Candidate
|
||||
}
|
||||
|
||||
func (p *candidatePair) String() string {
|
||||
@@ -83,7 +83,7 @@ func (p *candidatePair) Write(b []byte) (int, error) {
|
||||
}
|
||||
|
||||
// keepaliveCandidate sends a STUN Binding Indication to the remote candidate
|
||||
func (a *Agent) keepaliveCandidate(local, remote *Candidate) {
|
||||
func (a *Agent) keepaliveCandidate(local, remote Candidate) {
|
||||
msg, err := stun.Build(stun.NewType(stun.MethodBinding, stun.ClassIndication), stun.TransactionID,
|
||||
stun.NewUsername(a.remoteUfrag+":"+a.localUfrag),
|
||||
stun.NewShortTermIntegrity(a.remotePwd),
|
||||
@@ -98,7 +98,7 @@ func (a *Agent) keepaliveCandidate(local, remote *Candidate) {
|
||||
a.sendSTUN(msg, local, remote)
|
||||
}
|
||||
|
||||
func (a *Agent) sendSTUN(msg *stun.Message, local, remote *Candidate) {
|
||||
func (a *Agent) sendSTUN(msg *stun.Message, local, remote Candidate) {
|
||||
_, err := local.writeTo(msg.Raw, remote)
|
||||
if err != nil {
|
||||
a.log.Tracef("failed to send STUN message: %s", err)
|
||||
|
||||
+20
-16
@@ -3,25 +3,29 @@ package ice
|
||||
import "testing"
|
||||
|
||||
var (
|
||||
hostCandidate = &Candidate{
|
||||
Type: CandidateTypeHost,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
hostCandidate = &CandidateHost{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypeHost,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
}
|
||||
prflxCandidate = &Candidate{
|
||||
Type: CandidateTypePeerReflexive,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
prflxCandidate = &CandidatePeerReflexive{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypePeerReflexive,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
}
|
||||
srflxCandidate = &Candidate{
|
||||
Type: CandidateTypeServerReflexive,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
srflxCandidate = &CandidateServerReflexive{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypeServerReflexive,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
}
|
||||
relayCandidate = &Candidate{
|
||||
Type: CandidateTypeRelay,
|
||||
LocalPreference: defaultLocalPreference,
|
||||
Component: ComponentRTP,
|
||||
relayCandidate = &CandidateRelay{
|
||||
candidateBase: candidateBase{
|
||||
candidateType: CandidateTypeRelay,
|
||||
component: ComponentRTP,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -107,10 +107,9 @@ func gatherCandidatesLocal(a *Agent, networkTypes []NetworkType) {
|
||||
continue
|
||||
}
|
||||
|
||||
networkType := c.NetworkType
|
||||
set := a.localCandidates[networkType]
|
||||
set := a.localCandidates[c.NetworkType()]
|
||||
set = append(set, c)
|
||||
a.localCandidates[networkType] = set
|
||||
a.localCandidates[c.NetworkType()] = set
|
||||
|
||||
c.start(a, conn)
|
||||
}
|
||||
@@ -154,10 +153,9 @@ func gatherCandidatesReflective(a *Agent, urls []*URL, networkTypes []NetworkTyp
|
||||
continue
|
||||
}
|
||||
|
||||
networkType := c.NetworkType
|
||||
set := a.localCandidates[networkType]
|
||||
set := a.localCandidates[c.NetworkType()]
|
||||
set = append(set, c)
|
||||
a.localCandidates[networkType] = set
|
||||
a.localCandidates[c.NetworkType()] = set
|
||||
|
||||
c.start(a, conn)
|
||||
|
||||
|
||||
+9
-9
@@ -10,9 +10,9 @@ import (
|
||||
type pairCandidateSelector interface {
|
||||
Start()
|
||||
ContactCandidates()
|
||||
PingCandidate(local, remote *Candidate)
|
||||
HandleSucessResponse(m *stun.Message, local, remote *Candidate, remoteAddr net.Addr)
|
||||
HandleBindingRequest(m *stun.Message, local, remote *Candidate)
|
||||
PingCandidate(local, remote Candidate)
|
||||
HandleSucessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr)
|
||||
HandleBindingRequest(m *stun.Message, local, remote Candidate)
|
||||
}
|
||||
|
||||
type controllingSelector struct {
|
||||
@@ -62,7 +62,7 @@ func (s *controllingSelector) nominatePair(pair *candidatePair) {
|
||||
s.agent.sendBindingRequest(msg, pair.local, pair.remote)
|
||||
}
|
||||
|
||||
func (s *controllingSelector) HandleBindingRequest(m *stun.Message, local, remote *Candidate) {
|
||||
func (s *controllingSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) {
|
||||
s.agent.sendBindingSuccess(m, local, remote)
|
||||
|
||||
p := s.agent.findValidPair(local, remote)
|
||||
@@ -72,7 +72,7 @@ func (s *controllingSelector) HandleBindingRequest(m *stun.Message, local, remot
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controllingSelector) HandleSucessResponse(m *stun.Message, local, remote *Candidate, remoteAddr net.Addr) {
|
||||
func (s *controllingSelector) HandleSucessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) {
|
||||
ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID)
|
||||
if !ok {
|
||||
s.log.Errorf("discard message from (%s), invalid TransactionID %s", remote, m.TransactionID)
|
||||
@@ -96,7 +96,7 @@ func (s *controllingSelector) HandleSucessResponse(m *stun.Message, local, remot
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controllingSelector) PingCandidate(local, remote *Candidate) {
|
||||
func (s *controllingSelector) PingCandidate(local, remote Candidate) {
|
||||
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
|
||||
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
|
||||
AttrControlling(s.agent.tieBreaker),
|
||||
@@ -132,7 +132,7 @@ func (s *controlledSelector) ContactCandidates() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controlledSelector) PingCandidate(local, remote *Candidate) {
|
||||
func (s *controlledSelector) PingCandidate(local, remote Candidate) {
|
||||
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
|
||||
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
|
||||
AttrControlled(s.agent.tieBreaker),
|
||||
@@ -149,7 +149,7 @@ func (s *controlledSelector) PingCandidate(local, remote *Candidate) {
|
||||
s.agent.sendBindingRequest(msg, local, remote)
|
||||
}
|
||||
|
||||
func (s *controlledSelector) HandleSucessResponse(m *stun.Message, local, remote *Candidate, remoteAddr net.Addr) {
|
||||
func (s *controlledSelector) HandleSucessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) {
|
||||
// TODO according to the standard we should specifically answer a failed nomination:
|
||||
// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
|
||||
// If the controlled agent does not accept the request from the
|
||||
@@ -176,7 +176,7 @@ func (s *controlledSelector) HandleSucessResponse(m *stun.Message, local, remote
|
||||
s.agent.addValidPair(local, remote)
|
||||
}
|
||||
|
||||
func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote *Candidate) {
|
||||
func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) {
|
||||
if m.Contains(stun.AttrUseCandidate) {
|
||||
// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
|
||||
p := s.agent.findValidPair(local, remote)
|
||||
|
||||
+12
-14
@@ -269,22 +269,20 @@ func pipeWithTimeout(iceTimeout time.Duration, iceKeepalive time.Duration) (*Con
|
||||
return aConn, bConn
|
||||
}
|
||||
|
||||
func copyCandidate(orig *Candidate) *Candidate {
|
||||
c := &Candidate{
|
||||
Type: orig.Type,
|
||||
NetworkType: orig.NetworkType,
|
||||
IP: orig.IP,
|
||||
Port: orig.Port,
|
||||
}
|
||||
|
||||
if orig.RelatedAddress != nil {
|
||||
c.RelatedAddress = &CandidateRelatedAddress{
|
||||
Address: orig.RelatedAddress.Address,
|
||||
Port: orig.RelatedAddress.Port,
|
||||
func copyCandidate(o Candidate) Candidate {
|
||||
switch orig := o.(type) {
|
||||
case *CandidateHost:
|
||||
return &CandidateHost{
|
||||
candidateBase{
|
||||
networkType: orig.networkType,
|
||||
ip: orig.ip,
|
||||
port: orig.port,
|
||||
component: orig.component,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func onConnected() (func(ConnectionState), chan struct{}) {
|
||||
|
||||
Reference in New Issue
Block a user