mirror of
https://github.com/xjasonlyu/tun2socks.git
synced 2024-12-30 02:37:01 +00:00
add go-tun2socks code
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// Code in this file are grabbed from https://github.com/nadoo/glider, which
|
||||
// is also referencing another repo: https://github.com/shadowsocks/go-shadowsocks2
|
||||
|
||||
package socks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SOCKS request commands as defined in RFC 1928 section 4.
|
||||
const (
|
||||
socks5Connect = 1
|
||||
socks5Bind = 2
|
||||
socks5UDPAssociate = 3
|
||||
)
|
||||
|
||||
// SOCKS address types as defined in RFC 1928 section 5.
|
||||
const (
|
||||
socks5IP4 = 1
|
||||
socks5Domain = 3
|
||||
socks5IP6 = 4
|
||||
)
|
||||
|
||||
var socks5Errors = []error{
|
||||
errors.New(""),
|
||||
errors.New("general failure"),
|
||||
errors.New("connection forbidden"),
|
||||
errors.New("network unreachable"),
|
||||
errors.New("host unreachable"),
|
||||
errors.New("connection refused"),
|
||||
errors.New("TTL expired"),
|
||||
errors.New("command not supported"),
|
||||
errors.New("address type not supported"),
|
||||
errors.New("socks5UDPAssociate"),
|
||||
}
|
||||
|
||||
// MaxAddrLen is the maximum size of SOCKS address in bytes.
|
||||
const MaxAddrLen = 1 + 1 + 255 + 2
|
||||
|
||||
// ATYP return the address type
|
||||
func ATYP(b byte) int {
|
||||
return int(b &^ 0x8)
|
||||
}
|
||||
|
||||
// Addr represents a SOCKS address as defined in RFC 1928 section 5.
|
||||
type Addr []byte
|
||||
|
||||
// String serializes SOCKS address a to string form.
|
||||
func (a Addr) String() string {
|
||||
var host, port string
|
||||
|
||||
switch ATYP(a[0]) { // address type
|
||||
case socks5Domain:
|
||||
host = string(a[2 : 2+int(a[1])])
|
||||
port = strconv.Itoa((int(a[2+int(a[1])]) << 8) | int(a[2+int(a[1])+1]))
|
||||
case socks5IP4:
|
||||
host = net.IP(a[1 : 1+net.IPv4len]).String()
|
||||
port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1]))
|
||||
case socks5IP6:
|
||||
host = net.IP(a[1 : 1+net.IPv6len]).String()
|
||||
port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1]))
|
||||
}
|
||||
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
// ParseAddr parses the address in string s. Returns nil if failed.
|
||||
func ParseAddr(s string) Addr {
|
||||
var addr Addr
|
||||
host, port, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
addr = make([]byte, 1+net.IPv4len+2)
|
||||
addr[0] = socks5IP4
|
||||
copy(addr[1:], ip4)
|
||||
} else {
|
||||
addr = make([]byte, 1+net.IPv6len+2)
|
||||
addr[0] = socks5IP6
|
||||
copy(addr[1:], ip)
|
||||
}
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return nil
|
||||
}
|
||||
addr = make([]byte, 1+1+len(host)+2)
|
||||
addr[0] = socks5Domain
|
||||
addr[1] = byte(len(host))
|
||||
copy(addr[2:], host)
|
||||
}
|
||||
|
||||
portnum, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum)
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
func readAddr(r io.Reader, b []byte) (Addr, error) {
|
||||
if len(b) < MaxAddrLen {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
_, err := io.ReadFull(r, b[:1]) // read 1st byte for address type
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch ATYP(b[0]) {
|
||||
case socks5Domain:
|
||||
_, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = io.ReadFull(r, b[2:2+int(b[1])+2])
|
||||
return b[:1+1+int(b[1])+2], err
|
||||
case socks5IP4:
|
||||
_, err = io.ReadFull(r, b[1:1+net.IPv4len+2])
|
||||
return b[:1+net.IPv4len+2], err
|
||||
case socks5IP6:
|
||||
_, err = io.ReadFull(r, b[1:1+net.IPv6len+2])
|
||||
return b[:1+net.IPv6len+2], err
|
||||
}
|
||||
|
||||
return nil, socks5Errors[8]
|
||||
}
|
||||
|
||||
// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed.
|
||||
func SplitAddr(b []byte) Addr {
|
||||
addrLen := 1
|
||||
if len(b) < addrLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ATYP(b[0]) {
|
||||
case socks5Domain:
|
||||
if len(b) < 2 {
|
||||
return nil
|
||||
}
|
||||
addrLen = 1 + 1 + int(b[1]) + 2
|
||||
case socks5IP4:
|
||||
addrLen = 1 + net.IPv4len + 2
|
||||
case socks5IP6:
|
||||
addrLen = 1 + net.IPv6len + 2
|
||||
default:
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
if len(b) < addrLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b[:addrLen]
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
|
||||
"github.com/xjasonlyu/tun2socks/common/dns"
|
||||
"github.com/xjasonlyu/tun2socks/common/log"
|
||||
"github.com/xjasonlyu/tun2socks/common/lsof"
|
||||
"github.com/xjasonlyu/tun2socks/common/stats"
|
||||
"github.com/xjasonlyu/tun2socks/core"
|
||||
)
|
||||
|
||||
type tcpHandler struct {
|
||||
sync.Mutex
|
||||
|
||||
proxyHost string
|
||||
proxyPort uint16
|
||||
|
||||
fakeDns dns.FakeDns
|
||||
sessionStater stats.SessionStater
|
||||
}
|
||||
|
||||
func NewTCPHandler(proxyHost string, proxyPort uint16, fakeDns dns.FakeDns, sessionStater stats.SessionStater) core.TCPConnHandler {
|
||||
return &tcpHandler{
|
||||
proxyHost: proxyHost,
|
||||
proxyPort: proxyPort,
|
||||
fakeDns: fakeDns,
|
||||
sessionStater: sessionStater,
|
||||
}
|
||||
}
|
||||
|
||||
type direction byte
|
||||
|
||||
const (
|
||||
dirUplink direction = iota
|
||||
dirDownlink
|
||||
)
|
||||
|
||||
func statsCopy(dst io.Writer, src io.Reader, sess *stats.Session, dir direction) (written int64, err error) {
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write(buf[0:nr])
|
||||
if nw > 0 {
|
||||
switch dir {
|
||||
case dirUplink:
|
||||
sess.AddUploadBytes(int64(nw))
|
||||
case dirDownlink:
|
||||
sess.AddDownloadBytes(int64(nw))
|
||||
default:
|
||||
}
|
||||
written += int64(nw)
|
||||
}
|
||||
if ew != nil {
|
||||
err = ew
|
||||
break
|
||||
}
|
||||
if nr != nw {
|
||||
err = io.ErrShortWrite
|
||||
break
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
if er != io.EOF {
|
||||
err = er
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
type duplexConn interface {
|
||||
net.Conn
|
||||
CloseRead() error
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
func (h *tcpHandler) relay(lhs, rhs net.Conn, sess *stats.Session) {
|
||||
var err error
|
||||
upCh := make(chan struct{})
|
||||
|
||||
cls := func(dir direction, interrupt bool) {
|
||||
lhsDConn, lhsOk := lhs.(duplexConn)
|
||||
rhsDConn, rhsOk := rhs.(duplexConn)
|
||||
if !interrupt && lhsOk && rhsOk {
|
||||
switch dir {
|
||||
case dirUplink:
|
||||
_ = lhsDConn.CloseRead()
|
||||
_ = rhsDConn.CloseWrite()
|
||||
case dirDownlink:
|
||||
_ = lhsDConn.CloseWrite()
|
||||
_ = rhsDConn.CloseRead()
|
||||
default:
|
||||
panic("unexpected direction")
|
||||
}
|
||||
} else {
|
||||
_ = lhs.Close()
|
||||
_ = rhs.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Uplink
|
||||
go func() {
|
||||
if h.sessionStater != nil && sess != nil {
|
||||
_, err = statsCopy(rhs, lhs, sess, dirUplink)
|
||||
} else {
|
||||
_, err = io.Copy(rhs, lhs)
|
||||
}
|
||||
if err != nil {
|
||||
cls(dirUplink, true) // interrupt the conn if the error is not nil (not EOF)
|
||||
} else {
|
||||
cls(dirUplink, false) // half close uplink direction of the TCP conn if possible
|
||||
}
|
||||
upCh <- struct{}{}
|
||||
}()
|
||||
|
||||
// Downlink
|
||||
if h.sessionStater != nil && sess != nil {
|
||||
_, err = statsCopy(lhs, rhs, sess, dirDownlink)
|
||||
} else {
|
||||
_, err = io.Copy(lhs, rhs)
|
||||
}
|
||||
if err != nil {
|
||||
cls(dirDownlink, true)
|
||||
} else {
|
||||
cls(dirDownlink, false)
|
||||
}
|
||||
|
||||
<-upCh // Wait for uplink done.
|
||||
|
||||
if h.sessionStater != nil {
|
||||
h.sessionStater.RemoveSession(lhs)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *tcpHandler) Handle(conn net.Conn, target *net.TCPAddr) error {
|
||||
dialer, err := proxy.SOCKS5("tcp", core.ParseTCPAddr(h.proxyHost, h.proxyPort).String(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace with a domain name if target address IP is a fake IP.
|
||||
var targetHost string
|
||||
if h.fakeDns != nil && h.fakeDns.IsFakeIP(target.IP) {
|
||||
targetHost = h.fakeDns.IPToHost(target.IP)
|
||||
} else {
|
||||
targetHost = target.IP.String()
|
||||
}
|
||||
dest := net.JoinHostPort(targetHost, strconv.Itoa(target.Port))
|
||||
|
||||
c, err := dialer.Dial(target.Network(), dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var process string
|
||||
var sess *stats.Session
|
||||
if h.sessionStater != nil {
|
||||
// Get name of the process.
|
||||
localHost, localPortStr, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
localPortInt, _ := strconv.Atoi(localPortStr)
|
||||
process, err = lsof.GetCommandNameBySocket(target.Network(), localHost, uint16(localPortInt))
|
||||
if err != nil {
|
||||
process = "unknown process"
|
||||
}
|
||||
|
||||
sess = &stats.Session{
|
||||
ProcessName: process,
|
||||
Network: target.Network(),
|
||||
LocalAddr: conn.LocalAddr().String(),
|
||||
RemoteAddr: dest,
|
||||
UploadBytes: 0,
|
||||
DownloadBytes: 0,
|
||||
SessionStart: time.Now(),
|
||||
}
|
||||
h.sessionStater.AddSession(conn, sess)
|
||||
}
|
||||
|
||||
go h.relay(conn, c, sess)
|
||||
|
||||
log.Access(process, "proxy", target.Network(), conn.LocalAddr().String(), dest)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xjasonlyu/tun2socks/common/dns"
|
||||
"github.com/xjasonlyu/tun2socks/common/log"
|
||||
"github.com/xjasonlyu/tun2socks/common/lsof"
|
||||
"github.com/xjasonlyu/tun2socks/common/stats"
|
||||
"github.com/xjasonlyu/tun2socks/core"
|
||||
)
|
||||
|
||||
type udpHandler struct {
|
||||
sync.Mutex
|
||||
|
||||
proxyHost string
|
||||
proxyPort uint16
|
||||
udpConns map[core.UDPConn]net.PacketConn
|
||||
tcpConns map[core.UDPConn]net.Conn
|
||||
remoteAddrs map[core.UDPConn]*net.UDPAddr // UDP relay server addresses
|
||||
timeout time.Duration
|
||||
|
||||
dnsCache dns.DnsCache
|
||||
fakeDns dns.FakeDns
|
||||
sessionStater stats.SessionStater
|
||||
}
|
||||
|
||||
func NewUDPHandler(proxyHost string, proxyPort uint16, timeout time.Duration, dnsCache dns.DnsCache, fakeDns dns.FakeDns, sessionStater stats.SessionStater) core.UDPConnHandler {
|
||||
return &udpHandler{
|
||||
proxyHost: proxyHost,
|
||||
proxyPort: proxyPort,
|
||||
udpConns: make(map[core.UDPConn]net.PacketConn, 8),
|
||||
tcpConns: make(map[core.UDPConn]net.Conn, 8),
|
||||
remoteAddrs: make(map[core.UDPConn]*net.UDPAddr, 8),
|
||||
dnsCache: dnsCache,
|
||||
fakeDns: fakeDns,
|
||||
timeout: timeout,
|
||||
sessionStater: sessionStater,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *udpHandler) handleTCP(conn core.UDPConn, c net.Conn) {
|
||||
buf := core.NewBytes(core.BufSize)
|
||||
defer core.FreeBytes(buf)
|
||||
|
||||
for {
|
||||
_ = c.SetDeadline(time.Time{})
|
||||
_, err := c.Read(buf)
|
||||
if err == io.EOF {
|
||||
log.Warnf("UDP associate to %v closed by remote", c.RemoteAddr())
|
||||
h.Close(conn)
|
||||
return
|
||||
} else if err != nil {
|
||||
h.Close(conn)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *udpHandler) fetchUDPInput(conn core.UDPConn, input net.PacketConn) {
|
||||
buf := core.NewBytes(core.BufSize)
|
||||
|
||||
defer func() {
|
||||
h.Close(conn)
|
||||
core.FreeBytes(buf)
|
||||
}()
|
||||
|
||||
for {
|
||||
_ = input.SetDeadline(time.Now().Add(h.timeout))
|
||||
n, _, err := input.ReadFrom(buf)
|
||||
if err != nil {
|
||||
// log.Printf("read remote failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
addr := SplitAddr(buf[3:])
|
||||
resolvedAddr, err := net.ResolveUDPAddr("udp", addr.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n, err = conn.WriteFrom(buf[int(3+len(addr)):n], resolvedAddr)
|
||||
if n > 0 && h.sessionStater != nil {
|
||||
if sess := h.sessionStater.GetSession(conn); sess != nil {
|
||||
sess.AddDownloadBytes(int64(n))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("write local failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if h.dnsCache != nil {
|
||||
_, port, err := net.SplitHostPort(addr.String())
|
||||
if err != nil {
|
||||
panic("impossible error")
|
||||
}
|
||||
if port == strconv.Itoa(dns.CommonDnsPort) {
|
||||
h.dnsCache.Store(buf[int(3+len(addr)):n])
|
||||
return // DNS response
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *udpHandler) Connect(conn core.UDPConn, target *net.UDPAddr) error {
|
||||
if target == nil {
|
||||
return h.connectInternal(conn, "")
|
||||
}
|
||||
|
||||
// Replace with a domain name if target address IP is a fake IP.
|
||||
targetHost := target.IP.String()
|
||||
if h.fakeDns != nil {
|
||||
if target.Port == dns.CommonDnsPort {
|
||||
return nil // skip dns
|
||||
}
|
||||
if h.fakeDns.IsFakeIP(target.IP) {
|
||||
targetHost = h.fakeDns.IPToHost(target.IP)
|
||||
}
|
||||
}
|
||||
dest := net.JoinHostPort(targetHost, strconv.Itoa(target.Port))
|
||||
|
||||
return h.connectInternal(conn, dest)
|
||||
}
|
||||
|
||||
func (h *udpHandler) connectInternal(conn core.UDPConn, dest string) error {
|
||||
c, err := net.DialTimeout("tcp", core.ParseTCPAddr(h.proxyHost, h.proxyPort).String(), 4*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = c.SetDeadline(time.Now().Add(4 * time.Second))
|
||||
|
||||
// send VER, NMETHODS, METHODS
|
||||
_, _ = c.Write([]byte{5, 1, 0})
|
||||
|
||||
buf := make([]byte, MaxAddrLen)
|
||||
// read VER METHOD
|
||||
if _, err := io.ReadFull(c, buf[:2]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(dest) != 0 {
|
||||
targetAddr := ParseAddr(dest)
|
||||
// write VER CMD RSV ATYP DST.ADDR DST.PORT
|
||||
_, _ = c.Write(append([]byte{5, socks5UDPAssociate, 0}, targetAddr...))
|
||||
} else {
|
||||
_, _ = c.Write(append([]byte{5, socks5UDPAssociate, 0}, []byte{1, 0, 0, 0, 0, 0, 0}...))
|
||||
}
|
||||
|
||||
// read VER REP RSV ATYP BND.ADDR BND.PORT
|
||||
if _, err := io.ReadFull(c, buf[:3]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rep := buf[1]
|
||||
if rep != 0 {
|
||||
return errors.New("SOCKS handshake failed")
|
||||
}
|
||||
|
||||
remoteAddr, err := readAddr(c, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolvedRemoteAddr, err := net.ResolveUDPAddr("udp", remoteAddr.String())
|
||||
if err != nil {
|
||||
return errors.New("failed to resolve remote address")
|
||||
}
|
||||
|
||||
go h.handleTCP(conn, c)
|
||||
|
||||
pc, err := net.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
h.tcpConns[conn] = c
|
||||
h.udpConns[conn] = pc
|
||||
h.remoteAddrs[conn] = resolvedRemoteAddr
|
||||
h.Unlock()
|
||||
|
||||
go h.fetchUDPInput(conn, pc)
|
||||
|
||||
if len(dest) != 0 {
|
||||
var process string
|
||||
if h.sessionStater != nil {
|
||||
// Get name of the process.
|
||||
localHost, localPortStr, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
localPortInt, _ := strconv.Atoi(localPortStr)
|
||||
process, err = lsof.GetCommandNameBySocket(conn.LocalAddr().Network(), localHost, uint16(localPortInt))
|
||||
if err != nil {
|
||||
process = "unknown process"
|
||||
}
|
||||
|
||||
sess := &stats.Session{
|
||||
ProcessName: process,
|
||||
Network: conn.LocalAddr().Network(),
|
||||
LocalAddr: conn.LocalAddr().String(),
|
||||
RemoteAddr: dest,
|
||||
UploadBytes: 0,
|
||||
DownloadBytes: 0,
|
||||
SessionStart: time.Now(),
|
||||
}
|
||||
h.sessionStater.AddSession(conn, sess)
|
||||
}
|
||||
log.Access(process, "proxy", "udp", conn.LocalAddr().String(), dest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *udpHandler) ReceiveTo(conn core.UDPConn, data []byte, addr *net.UDPAddr) error {
|
||||
h.Lock()
|
||||
pc, ok1 := h.udpConns[conn]
|
||||
remoteAddr, ok2 := h.remoteAddrs[conn]
|
||||
h.Unlock()
|
||||
|
||||
// use system DNS instead of force override
|
||||
if ok1 && ok2 {
|
||||
var targetHost string
|
||||
if h.fakeDns != nil && h.fakeDns.IsFakeIP(addr.IP) {
|
||||
targetHost = h.fakeDns.IPToHost(addr.IP)
|
||||
} else {
|
||||
targetHost = addr.IP.String()
|
||||
}
|
||||
dest := net.JoinHostPort(targetHost, strconv.Itoa(addr.Port))
|
||||
|
||||
buf := append([]byte{0, 0, 0}, ParseAddr(dest)...)
|
||||
buf = append(buf, data[:]...)
|
||||
n, err := pc.WriteTo(buf, remoteAddr)
|
||||
if n > 0 && h.sessionStater != nil {
|
||||
if sess := h.sessionStater.GetSession(conn); sess != nil {
|
||||
sess.AddUploadBytes(int64(n))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
h.Close(conn)
|
||||
return errors.New(fmt.Sprintf("write remote failed: %v", err))
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
h.Close(conn)
|
||||
return errors.New(fmt.Sprintf("proxy connection %v->%v does not exists", conn.LocalAddr(), addr))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *udpHandler) Close(conn core.UDPConn) {
|
||||
_ = conn.Close()
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if c, ok := h.tcpConns[conn]; ok {
|
||||
_ = c.Close()
|
||||
delete(h.tcpConns, conn)
|
||||
}
|
||||
if pc, ok := h.udpConns[conn]; ok {
|
||||
_ = pc.Close()
|
||||
delete(h.udpConns, conn)
|
||||
}
|
||||
delete(h.remoteAddrs, conn)
|
||||
|
||||
if h.sessionStater != nil {
|
||||
h.sessionStater.RemoveSession(conn)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user