Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49bf5b80c8 | ||
|
|
24852b6b3f | ||
|
|
bbc689cf5c | ||
|
|
7292932d45 |
@@ -4,8 +4,8 @@ go 1.23.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/net v0.21.0 // tagx:ignore
|
golang.org/x/net v0.21.0 // tagx:ignore
|
||||||
golang.org/x/sys v0.30.0
|
golang.org/x/sys v0.31.0
|
||||||
golang.org/x/term v0.29.0
|
golang.org/x/term v0.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.22.0 // indirect
|
require golang.org/x/text v0.23.0 // indirect
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
|||||||
+37
-10
@@ -25,6 +25,11 @@ const debugHandshake = false
|
|||||||
// quickly.
|
// quickly.
|
||||||
const chanSize = 16
|
const chanSize = 16
|
||||||
|
|
||||||
|
// maxPendingPackets sets the maximum number of packets to queue while waiting
|
||||||
|
// for KEX to complete. This limits the total pending data to maxPendingPackets
|
||||||
|
// * maxPacket bytes, which is ~16.8MB.
|
||||||
|
const maxPendingPackets = 64
|
||||||
|
|
||||||
// keyingTransport is a packet based transport that supports key
|
// keyingTransport is a packet based transport that supports key
|
||||||
// changes. It need not be thread-safe. It should pass through
|
// changes. It need not be thread-safe. It should pass through
|
||||||
// msgNewKeys in both directions.
|
// msgNewKeys in both directions.
|
||||||
@@ -73,11 +78,19 @@ type handshakeTransport struct {
|
|||||||
incoming chan []byte
|
incoming chan []byte
|
||||||
readError error
|
readError error
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
writeError error
|
// Condition for the above mutex. It is used to notify a completed key
|
||||||
sentInitPacket []byte
|
// exchange or a write failure. Writes can wait for this condition while a
|
||||||
sentInitMsg *kexInitMsg
|
// key exchange is in progress.
|
||||||
pendingPackets [][]byte // Used when a key exchange is in progress.
|
writeCond *sync.Cond
|
||||||
|
writeError error
|
||||||
|
sentInitPacket []byte
|
||||||
|
sentInitMsg *kexInitMsg
|
||||||
|
// Used to queue writes when a key exchange is in progress. The length is
|
||||||
|
// limited by pendingPacketsSize. Once full, writes will block until the key
|
||||||
|
// exchange is completed or an error occurs. If not empty, it is emptied
|
||||||
|
// all at once when the key exchange is completed in kexLoop.
|
||||||
|
pendingPackets [][]byte
|
||||||
writePacketsLeft uint32
|
writePacketsLeft uint32
|
||||||
writeBytesLeft int64
|
writeBytesLeft int64
|
||||||
userAuthComplete bool // whether the user authentication phase is complete
|
userAuthComplete bool // whether the user authentication phase is complete
|
||||||
@@ -134,6 +147,7 @@ func newHandshakeTransport(conn keyingTransport, config *Config, clientVersion,
|
|||||||
|
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
t.writeCond = sync.NewCond(&t.mu)
|
||||||
t.resetReadThresholds()
|
t.resetReadThresholds()
|
||||||
t.resetWriteThresholds()
|
t.resetWriteThresholds()
|
||||||
|
|
||||||
@@ -260,6 +274,7 @@ func (t *handshakeTransport) recordWriteError(err error) {
|
|||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
if t.writeError == nil && err != nil {
|
if t.writeError == nil && err != nil {
|
||||||
t.writeError = err
|
t.writeError = err
|
||||||
|
t.writeCond.Broadcast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +378,8 @@ write:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.pendingPackets = t.pendingPackets[:0]
|
t.pendingPackets = t.pendingPackets[:0]
|
||||||
|
// Unblock writePacket if waiting for KEX.
|
||||||
|
t.writeCond.Broadcast()
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,11 +594,20 @@ func (t *handshakeTransport) writePacket(p []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if t.sentInitMsg != nil {
|
if t.sentInitMsg != nil {
|
||||||
// Copy the packet so the writer can reuse the buffer.
|
if len(t.pendingPackets) < maxPendingPackets {
|
||||||
cp := make([]byte, len(p))
|
// Copy the packet so the writer can reuse the buffer.
|
||||||
copy(cp, p)
|
cp := make([]byte, len(p))
|
||||||
t.pendingPackets = append(t.pendingPackets, cp)
|
copy(cp, p)
|
||||||
return nil
|
t.pendingPackets = append(t.pendingPackets, cp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for t.sentInitMsg != nil {
|
||||||
|
// Block and wait for KEX to complete or an error.
|
||||||
|
t.writeCond.Wait()
|
||||||
|
if t.writeError != nil {
|
||||||
|
return t.writeError
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.writeBytesLeft > 0 {
|
if t.writeBytesLeft > 0 {
|
||||||
@@ -598,6 +624,7 @@ func (t *handshakeTransport) writePacket(p []byte) error {
|
|||||||
|
|
||||||
if err := t.pushPacket(p); err != nil {
|
if err := t.pushPacket(p); err != nil {
|
||||||
t.writeError = err
|
t.writeError = err
|
||||||
|
t.writeCond.Broadcast()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -539,6 +539,226 @@ func TestDisconnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockKeyingTransport struct {
|
||||||
|
packetConn
|
||||||
|
kexInitAllowed chan struct{}
|
||||||
|
kexInitSent chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *mockKeyingTransport) prepareKeyChange(*algorithms, *kexResult) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *mockKeyingTransport) writePacket(packet []byte) error {
|
||||||
|
if packet[0] == msgKexInit {
|
||||||
|
<-n.kexInitAllowed
|
||||||
|
n.kexInitSent <- struct{}{}
|
||||||
|
}
|
||||||
|
return n.packetConn.writePacket(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *mockKeyingTransport) readPacket() ([]byte, error) {
|
||||||
|
return n.packetConn.readPacket()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *mockKeyingTransport) setStrictMode() error { return nil }
|
||||||
|
|
||||||
|
func (n *mockKeyingTransport) setInitialKEXDone() {}
|
||||||
|
|
||||||
|
func TestHandshakePendingPacketsWait(t *testing.T) {
|
||||||
|
a, b := memPipe()
|
||||||
|
|
||||||
|
trS := &mockKeyingTransport{
|
||||||
|
packetConn: a,
|
||||||
|
kexInitAllowed: make(chan struct{}, 2),
|
||||||
|
kexInitSent: make(chan struct{}, 2),
|
||||||
|
}
|
||||||
|
// Allow the first KEX.
|
||||||
|
trS.kexInitAllowed <- struct{}{}
|
||||||
|
|
||||||
|
trC := &mockKeyingTransport{
|
||||||
|
packetConn: b,
|
||||||
|
kexInitAllowed: make(chan struct{}, 2),
|
||||||
|
kexInitSent: make(chan struct{}, 2),
|
||||||
|
}
|
||||||
|
// Allow the first KEX.
|
||||||
|
trC.kexInitAllowed <- struct{}{}
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
clientConf.SetDefaults()
|
||||||
|
|
||||||
|
v := []byte("version")
|
||||||
|
client := newClientTransport(trC, v, v, clientConf, "addr", nil)
|
||||||
|
|
||||||
|
serverConf := &ServerConfig{}
|
||||||
|
serverConf.AddHostKey(testSigners["ecdsa"])
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
serverConf.SetDefaults()
|
||||||
|
server := newServerTransport(trS, v, v, serverConf)
|
||||||
|
|
||||||
|
if err := server.waitSession(); err != nil {
|
||||||
|
t.Fatalf("server.waitSession: %v", err)
|
||||||
|
}
|
||||||
|
if err := client.waitSession(); err != nil {
|
||||||
|
t.Fatalf("client.waitSession: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-trC.kexInitSent
|
||||||
|
<-trS.kexInitSent
|
||||||
|
|
||||||
|
// Allow and request new KEX server side.
|
||||||
|
trS.kexInitAllowed <- struct{}{}
|
||||||
|
server.requestKeyExchange()
|
||||||
|
// Wait until the KEX init is sent.
|
||||||
|
<-trS.kexInitSent
|
||||||
|
// The client is not allowed to respond to the KEX, so writes will be
|
||||||
|
// blocked on the server side once the packets queue is full.
|
||||||
|
for i := 0; i < maxPendingPackets; i++ {
|
||||||
|
p := []byte{msgRequestSuccess, byte(i)}
|
||||||
|
if err := server.writePacket(p); err != nil {
|
||||||
|
t.Errorf("unexpected write error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The packets queue is now full, the next write will block.
|
||||||
|
server.mu.Lock()
|
||||||
|
if len(server.pendingPackets) != maxPendingPackets {
|
||||||
|
t.Errorf("unexpected pending packets size; got: %d, want: %d", len(server.pendingPackets), maxPendingPackets)
|
||||||
|
}
|
||||||
|
server.mu.Unlock()
|
||||||
|
|
||||||
|
writeDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(writeDone)
|
||||||
|
|
||||||
|
p := []byte{msgRequestSuccess, byte(65)}
|
||||||
|
// This write will block until KEX completes.
|
||||||
|
err := server.writePacket(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected write error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Consume packets on the client side
|
||||||
|
readDone := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
defer close(readDone)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, err := client.readPacket(); err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("unexpected read error: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Allow the client to reply to the KEX and so unblock the write goroutine.
|
||||||
|
trC.kexInitAllowed <- struct{}{}
|
||||||
|
<-trC.kexInitSent
|
||||||
|
<-writeDone
|
||||||
|
// Close the client to unblock the read goroutine.
|
||||||
|
client.Close()
|
||||||
|
<-readDone
|
||||||
|
server.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandshakePendingPacketsError(t *testing.T) {
|
||||||
|
a, b := memPipe()
|
||||||
|
|
||||||
|
trS := &mockKeyingTransport{
|
||||||
|
packetConn: a,
|
||||||
|
kexInitAllowed: make(chan struct{}, 2),
|
||||||
|
kexInitSent: make(chan struct{}, 2),
|
||||||
|
}
|
||||||
|
// Allow the first KEX.
|
||||||
|
trS.kexInitAllowed <- struct{}{}
|
||||||
|
|
||||||
|
trC := &mockKeyingTransport{
|
||||||
|
packetConn: b,
|
||||||
|
kexInitAllowed: make(chan struct{}, 2),
|
||||||
|
kexInitSent: make(chan struct{}, 2),
|
||||||
|
}
|
||||||
|
// Allow the first KEX.
|
||||||
|
trC.kexInitAllowed <- struct{}{}
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
clientConf.SetDefaults()
|
||||||
|
|
||||||
|
v := []byte("version")
|
||||||
|
client := newClientTransport(trC, v, v, clientConf, "addr", nil)
|
||||||
|
|
||||||
|
serverConf := &ServerConfig{}
|
||||||
|
serverConf.AddHostKey(testSigners["ecdsa"])
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
serverConf.SetDefaults()
|
||||||
|
server := newServerTransport(trS, v, v, serverConf)
|
||||||
|
|
||||||
|
if err := server.waitSession(); err != nil {
|
||||||
|
t.Fatalf("server.waitSession: %v", err)
|
||||||
|
}
|
||||||
|
if err := client.waitSession(); err != nil {
|
||||||
|
t.Fatalf("client.waitSession: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-trC.kexInitSent
|
||||||
|
<-trS.kexInitSent
|
||||||
|
|
||||||
|
// Allow and request new KEX server side.
|
||||||
|
trS.kexInitAllowed <- struct{}{}
|
||||||
|
server.requestKeyExchange()
|
||||||
|
// Wait until the KEX init is sent.
|
||||||
|
<-trS.kexInitSent
|
||||||
|
// The client is not allowed to respond to the KEX, so writes will be
|
||||||
|
// blocked on the server side once the packets queue is full.
|
||||||
|
for i := 0; i < maxPendingPackets; i++ {
|
||||||
|
p := []byte{msgRequestSuccess, byte(i)}
|
||||||
|
if err := server.writePacket(p); err != nil {
|
||||||
|
t.Errorf("unexpected write error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The packets queue is now full, the next write will block.
|
||||||
|
writeDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(writeDone)
|
||||||
|
|
||||||
|
p := []byte{msgRequestSuccess, byte(65)}
|
||||||
|
// This write will block until KEX completes.
|
||||||
|
err := server.writePacket(p)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("unexpected write error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Consume packets on the client side
|
||||||
|
readDone := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
defer close(readDone)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, err := client.readPacket(); err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("unexpected read error: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Close the server to unblock the write after an error
|
||||||
|
server.Close()
|
||||||
|
<-writeDone
|
||||||
|
// Unblock the pending write and close the client to unblock the read
|
||||||
|
// goroutine.
|
||||||
|
trC.kexInitAllowed <- struct{}{}
|
||||||
|
client.Close()
|
||||||
|
<-readDone
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandshakeRekeyDefault(t *testing.T) {
|
func TestHandshakeRekeyDefault(t *testing.T) {
|
||||||
clientConf := &ClientConfig{
|
clientConf := &ClientConfig{
|
||||||
Config: Config{
|
Config: Config{
|
||||||
|
|||||||
@@ -818,6 +818,8 @@ func decode(packet []byte) (interface{}, error) {
|
|||||||
return new(userAuthSuccessMsg), nil
|
return new(userAuthSuccessMsg), nil
|
||||||
case msgUserAuthFailure:
|
case msgUserAuthFailure:
|
||||||
msg = new(userAuthFailureMsg)
|
msg = new(userAuthFailureMsg)
|
||||||
|
case msgUserAuthBanner:
|
||||||
|
msg = new(userAuthBannerMsg)
|
||||||
case msgUserAuthPubKeyOk:
|
case msgUserAuthPubKeyOk:
|
||||||
msg = new(userAuthPubKeyOkMsg)
|
msg = new(userAuthPubKeyOkMsg)
|
||||||
case msgGlobalRequest:
|
case msgGlobalRequest:
|
||||||
|
|||||||
@@ -206,6 +206,62 @@ func TestMarshalMultiTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
rnd := rand.New(rand.NewSource(0))
|
||||||
|
kexInit := new(kexInitMsg).Generate(rnd, 10).Interface()
|
||||||
|
kexDHInit := new(kexDHInitMsg).Generate(rnd, 10).Interface()
|
||||||
|
kexDHReply := new(kexDHReplyMsg)
|
||||||
|
kexDHReply.Y = randomInt(rnd)
|
||||||
|
// Note: userAuthSuccessMsg can't be tested directly since it
|
||||||
|
// doesn't have a field for sshtype. So it's tested separately
|
||||||
|
// at the end.
|
||||||
|
decodeMessageTypes := []interface{}{
|
||||||
|
new(disconnectMsg),
|
||||||
|
new(serviceRequestMsg),
|
||||||
|
new(serviceAcceptMsg),
|
||||||
|
new(extInfoMsg),
|
||||||
|
kexInit,
|
||||||
|
kexDHInit,
|
||||||
|
kexDHReply,
|
||||||
|
new(userAuthRequestMsg),
|
||||||
|
new(userAuthFailureMsg),
|
||||||
|
new(userAuthBannerMsg),
|
||||||
|
new(userAuthPubKeyOkMsg),
|
||||||
|
new(globalRequestMsg),
|
||||||
|
new(globalRequestSuccessMsg),
|
||||||
|
new(globalRequestFailureMsg),
|
||||||
|
new(channelOpenMsg),
|
||||||
|
new(channelDataMsg),
|
||||||
|
new(channelOpenConfirmMsg),
|
||||||
|
new(channelOpenFailureMsg),
|
||||||
|
new(windowAdjustMsg),
|
||||||
|
new(channelEOFMsg),
|
||||||
|
new(channelCloseMsg),
|
||||||
|
new(channelRequestMsg),
|
||||||
|
new(channelRequestSuccessMsg),
|
||||||
|
new(channelRequestFailureMsg),
|
||||||
|
new(userAuthGSSAPIToken),
|
||||||
|
new(userAuthGSSAPIMIC),
|
||||||
|
new(userAuthGSSAPIErrTok),
|
||||||
|
new(userAuthGSSAPIError),
|
||||||
|
}
|
||||||
|
for _, msg := range decodeMessageTypes {
|
||||||
|
decoded, err := decode(Marshal(msg))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error decoding %T", msg)
|
||||||
|
} else if reflect.TypeOf(msg) != reflect.TypeOf(decoded) {
|
||||||
|
t.Errorf("error decoding %T, unexpected %T", msg, decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuthSuccess, err := decode([]byte{msgUserAuthSuccess})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error decoding userAuthSuccessMsg")
|
||||||
|
} else if reflect.TypeOf(userAuthSuccess) != reflect.TypeOf((*userAuthSuccessMsg)(nil)) {
|
||||||
|
t.Errorf("error decoding userAuthSuccessMsg, unexpected %T", userAuthSuccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func randomBytes(out []byte, rand *rand.Rand) {
|
func randomBytes(out []byte, rand *rand.Rand) {
|
||||||
for i := 0; i < len(out); i++ {
|
for i := 0; i < len(out); i++ {
|
||||||
out[i] = byte(rand.Int31())
|
out[i] = byte(rand.Int31())
|
||||||
|
|||||||
+1
-1
@@ -459,7 +459,7 @@ func (c *Client) dial(laddr string, lport int, raddr string, rport int) (Channel
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
go DiscardRequests(in)
|
go DiscardRequests(in)
|
||||||
return ch, err
|
return ch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type tcpChan struct {
|
type tcpChan struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user