633 lines
17 KiB
Go
633 lines
17 KiB
Go
package main
|
|
|
|
// This file is the LiteVPN v4 XOR chunk server, carried over verbatim. It
|
|
// handles the legacy UP/OK + XOR 0xAD wire (COPEN / CPUSH / CPULL / CCLOSE and
|
|
// the TUNNEL stream commands) so one server accepts both wire formats. The
|
|
// binary record handler lives in chunk.go; nothing here is shared with it
|
|
// except the target dialler, DNS cache, token check and debug counters.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"dragontcp/internal/protocol"
|
|
)
|
|
|
|
type chunkSession struct {
|
|
id string
|
|
target net.Conn
|
|
maxChunk int
|
|
maxChunks int
|
|
|
|
mu sync.Mutex
|
|
notify chan struct{}
|
|
chunks map[uint64][]byte
|
|
nextDown uint64
|
|
eof bool
|
|
closed bool
|
|
lastSeen time.Time
|
|
debug *serverDebug
|
|
|
|
upMu sync.Mutex
|
|
expectedUp uint64
|
|
lastUpSeq uint64
|
|
lastUpLen int
|
|
haveLastUp bool
|
|
}
|
|
|
|
func newChunkSession(id string, target net.Conn, maxChunk, maxChunks int, debug *serverDebug) *chunkSession {
|
|
s := &chunkSession{
|
|
id: id,
|
|
target: target,
|
|
maxChunk: maxChunk,
|
|
maxChunks: maxChunks,
|
|
notify: make(chan struct{}),
|
|
chunks: make(map[uint64][]byte, maxChunks),
|
|
lastSeen: time.Now(),
|
|
debug: debug,
|
|
}
|
|
go s.readTarget()
|
|
return s
|
|
}
|
|
|
|
func (s *chunkSession) signalLocked() {
|
|
close(s.notify)
|
|
s.notify = make(chan struct{})
|
|
}
|
|
|
|
func (s *chunkSession) touchLocked() {
|
|
s.lastSeen = time.Now()
|
|
}
|
|
|
|
func (s *chunkSession) touch() {
|
|
s.mu.Lock()
|
|
s.touchLocked()
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *chunkSession) readTarget() {
|
|
buf := make([]byte, s.maxChunk)
|
|
|
|
for {
|
|
n, err := s.target.Read(buf)
|
|
if n > 0 {
|
|
data := append([]byte(nil), buf[:n]...)
|
|
if s.debug != nil && s.debug.enabled {
|
|
s.debug.bytesDown.Add(uint64(n))
|
|
}
|
|
|
|
for {
|
|
s.mu.Lock()
|
|
if s.closed {
|
|
s.mu.Unlock()
|
|
return
|
|
}
|
|
if len(s.chunks) < s.maxChunks {
|
|
seq := s.nextDown
|
|
s.nextDown++
|
|
s.chunks[seq] = data
|
|
s.touchLocked()
|
|
s.signalLocked()
|
|
s.mu.Unlock()
|
|
break
|
|
}
|
|
ch := s.notify
|
|
s.mu.Unlock()
|
|
<-ch
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
if s.debug != nil && s.debug.enabled {
|
|
s.debug.logf("TARGET EOF session=%s err=%v", s.id, err)
|
|
}
|
|
s.mu.Lock()
|
|
if !s.closed {
|
|
s.eof = true
|
|
s.touchLocked()
|
|
s.signalLocked()
|
|
}
|
|
s.mu.Unlock()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// push is idempotent for the most recently accepted sequence. This matters
|
|
// when the server receives a record but the tiny ACK is lost: the client can
|
|
// retry the same sequence at a smaller adaptive size without duplicating bytes
|
|
// in the target stream. The ACK reports the length that was actually accepted.
|
|
func (s *chunkSession) push(seq uint64, data []byte) (int, error) {
|
|
s.upMu.Lock()
|
|
defer s.upMu.Unlock()
|
|
|
|
if len(data) == 0 || len(data) > s.maxChunk {
|
|
return 0, fmt.Errorf("upload record size %d is invalid", len(data))
|
|
}
|
|
|
|
if s.haveLastUp && seq == s.lastUpSeq {
|
|
s.touch()
|
|
return s.lastUpLen, nil
|
|
}
|
|
|
|
if seq < s.expectedUp {
|
|
return 0, fmt.Errorf("upload sequence %d is too old", seq)
|
|
}
|
|
if seq > s.expectedUp {
|
|
return 0, fmt.Errorf("unexpected upload sequence %d, expected %d", seq, s.expectedUp)
|
|
}
|
|
|
|
if _, err := s.target.Write(data); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if s.debug != nil && s.debug.enabled {
|
|
s.debug.bytesUp.Add(uint64(len(data)))
|
|
s.debug.pushRecords.Add(1)
|
|
}
|
|
|
|
s.lastUpSeq = seq
|
|
s.lastUpLen = len(data)
|
|
s.haveLastUp = true
|
|
s.expectedUp++
|
|
s.touch()
|
|
return len(data), nil
|
|
}
|
|
|
|
// pull returns at most limit bytes from the requested stored chunk, beginning
|
|
// at offset. The chunk sequence stays stable while the client retries smaller
|
|
// fragments, so a large queued chunk can always be recovered after an MTU-like
|
|
// failure without reopening the proxied destination connection.
|
|
func (s *chunkSession) pull(want uint64, ack int64, offset, limit int, wait time.Duration) (data []byte, total int, eof bool, final uint64, waitExpired bool, err error) {
|
|
if offset < 0 || limit <= 0 || limit > s.maxChunk {
|
|
return nil, 0, false, 0, false, fmt.Errorf("invalid pull offset/limit")
|
|
}
|
|
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
|
|
for {
|
|
s.mu.Lock()
|
|
s.touchLocked()
|
|
|
|
if ack >= 0 {
|
|
removed := false
|
|
for seq := range s.chunks {
|
|
if seq <= uint64(ack) {
|
|
delete(s.chunks, seq)
|
|
removed = true
|
|
}
|
|
}
|
|
if removed {
|
|
s.signalLocked()
|
|
}
|
|
}
|
|
|
|
if chunk, ok := s.chunks[want]; ok {
|
|
if offset >= len(chunk) {
|
|
s.mu.Unlock()
|
|
return nil, len(chunk), false, 0, false, fmt.Errorf("pull offset %d beyond chunk size %d", offset, len(chunk))
|
|
}
|
|
end := offset + limit
|
|
if end > len(chunk) {
|
|
end = len(chunk)
|
|
}
|
|
out := append([]byte(nil), chunk[offset:end]...)
|
|
total = len(chunk)
|
|
s.mu.Unlock()
|
|
return out, total, false, 0, false, nil
|
|
}
|
|
|
|
if s.eof && want >= s.nextDown {
|
|
final = s.nextDown
|
|
s.mu.Unlock()
|
|
return nil, 0, true, final, false, nil
|
|
}
|
|
|
|
if s.closed {
|
|
final = s.nextDown
|
|
s.mu.Unlock()
|
|
return nil, 0, true, final, false, nil
|
|
}
|
|
|
|
ch := s.notify
|
|
s.mu.Unlock()
|
|
|
|
select {
|
|
case <-ch:
|
|
continue
|
|
case <-timer.C:
|
|
return nil, 0, false, 0, true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *chunkSession) close() {
|
|
s.mu.Lock()
|
|
if s.closed {
|
|
s.mu.Unlock()
|
|
return
|
|
}
|
|
s.closed = true
|
|
s.signalLocked()
|
|
s.mu.Unlock()
|
|
_ = s.target.Close()
|
|
}
|
|
|
|
type chunkManager struct {
|
|
mu sync.RWMutex
|
|
sessions map[string]*chunkSession
|
|
timeout time.Duration
|
|
debug *serverDebug
|
|
}
|
|
|
|
func newChunkManager(timeout time.Duration, debug *serverDebug) *chunkManager {
|
|
m := &chunkManager{
|
|
sessions: make(map[string]*chunkSession),
|
|
timeout: timeout,
|
|
debug: debug,
|
|
}
|
|
go m.cleanupLoop()
|
|
return m
|
|
}
|
|
|
|
func (m *chunkManager) get(id string) *chunkSession {
|
|
m.mu.RLock()
|
|
s := m.sessions[id]
|
|
m.mu.RUnlock()
|
|
return s
|
|
}
|
|
|
|
func (m *chunkManager) count() int {
|
|
m.mu.RLock()
|
|
n := len(m.sessions)
|
|
m.mu.RUnlock()
|
|
return n
|
|
}
|
|
|
|
func (m *chunkManager) add(id string, s *chunkSession) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if _, exists := m.sessions[id]; exists {
|
|
return fmt.Errorf("session already exists")
|
|
}
|
|
m.sessions[id] = s
|
|
return nil
|
|
}
|
|
|
|
func (m *chunkManager) remove(id string) {
|
|
m.mu.Lock()
|
|
s := m.sessions[id]
|
|
delete(m.sessions, id)
|
|
m.mu.Unlock()
|
|
if s != nil {
|
|
s.close()
|
|
}
|
|
}
|
|
|
|
func (m *chunkManager) cleanupLoop() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
cutoff := time.Now().Add(-m.timeout)
|
|
var stale []string
|
|
|
|
m.mu.RLock()
|
|
for id, s := range m.sessions {
|
|
s.mu.Lock()
|
|
last := s.lastSeen
|
|
closed := s.closed
|
|
s.mu.Unlock()
|
|
if closed || last.Before(cutoff) {
|
|
stale = append(stale, id)
|
|
}
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
for _, id := range stale {
|
|
if m.debug != nil && m.debug.enabled {
|
|
m.debug.logf("SESSION timeout-close id=%s active_sessions=%d", id, m.count())
|
|
}
|
|
m.remove(id)
|
|
if m.debug != nil && m.debug.enabled {
|
|
m.debug.sessionsClosed.Add(1)
|
|
m.debug.activeSessions.Add(-1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func decodeWireToken(token string) string {
|
|
if token == "-" {
|
|
return ""
|
|
}
|
|
return token
|
|
}
|
|
|
|
func isChunkCommand(payload []byte) bool {
|
|
return bytes.HasPrefix(payload, []byte("COPEN ")) ||
|
|
bytes.HasPrefix(payload, []byte("CPUSH ")) ||
|
|
bytes.HasPrefix(payload, []byte("CPULL ")) ||
|
|
bytes.HasPrefix(payload, []byte("CCLOSE "))
|
|
}
|
|
|
|
func processChunkCommand(
|
|
conn net.Conn,
|
|
requestID uint32,
|
|
payload []byte,
|
|
token string,
|
|
allowPrivate bool,
|
|
cache *dnsCache,
|
|
tcpBuffer int,
|
|
manager *chunkManager,
|
|
maxChunk int,
|
|
maxBufferedChunks int,
|
|
pollWait time.Duration,
|
|
debug *serverDebug,
|
|
) error {
|
|
if bytes.HasPrefix(payload, []byte("COPEN ")) {
|
|
parts := strings.Fields(string(payload))
|
|
if len(parts) != 5 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad COPEN"))
|
|
}
|
|
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
|
}
|
|
sid := parts[2]
|
|
if len(sid) < 16 || len(sid) > 64 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid session id"))
|
|
}
|
|
host := parts[3]
|
|
port, err := strconv.Atoi(parts[4])
|
|
if err != nil || port < 1 || port > 65535 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid port"))
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
target, err := dialTarget(ctx, host, port, allowPrivate, cache, tcpBuffer)
|
|
cancel()
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
|
}
|
|
|
|
session := newChunkSession(sid, target, maxChunk, maxBufferedChunks, debug)
|
|
if err := manager.add(sid, session); err != nil {
|
|
session.close()
|
|
if debug != nil && debug.enabled {
|
|
debug.errorf("COPEN session=%s target=%s:%d failed: %v", sid, host, port, err)
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
|
}
|
|
if debug != nil && debug.enabled {
|
|
debug.sessionsOpened.Add(1)
|
|
debug.activeSessions.Add(1)
|
|
debug.logf("SESSION OPEN id=%s peer=%v target=%s:%d max_chunk=%d active_sessions=%d", sid, conn.RemoteAddr(), host, port, maxChunk, manager.count())
|
|
debug.chunkf("COPEN id=%s target=%s:%d -> OPENED max=%d", sid, host, port, maxChunk)
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("OPENED %d", maxChunk)))
|
|
}
|
|
|
|
if bytes.HasPrefix(payload, []byte("CPUSH ")) {
|
|
parts := bytes.SplitN(payload, []byte(" "), 5)
|
|
if len(parts) != 5 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPUSH"))
|
|
}
|
|
if !tokenEqual(decodeWireToken(string(parts[1])), token) {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
|
}
|
|
sid := string(parts[2])
|
|
seq, err := strconv.ParseUint(string(parts[3]), 10, 64)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid sequence"))
|
|
}
|
|
s := manager.get(sid)
|
|
if s == nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
|
}
|
|
accepted, err := s.push(seq, parts[4])
|
|
if err != nil {
|
|
if debug != nil && debug.enabled {
|
|
debug.errorf("CPUSH id=%s seq=%d bytes=%d: %v", sid, seq, len(parts[4]), err)
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
|
}
|
|
if debug != nil {
|
|
debug.chunkf("CPUSH id=%s seq=%d bytes=%d -> ACK accepted=%d", sid, seq, len(parts[4]), accepted)
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("ACK %d %d", seq, accepted)))
|
|
}
|
|
|
|
if bytes.HasPrefix(payload, []byte("CPULL ")) {
|
|
parts := strings.Fields(string(payload))
|
|
if len(parts) != 7 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CPULL"))
|
|
}
|
|
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
|
}
|
|
s := manager.get(parts[2])
|
|
if s == nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown session"))
|
|
}
|
|
ack, err := strconv.ParseInt(parts[3], 10, 64)
|
|
if err != nil || ack < -1 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid ack"))
|
|
}
|
|
want, err := strconv.ParseUint(parts[4], 10, 64)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid want"))
|
|
}
|
|
offset, err := strconv.Atoi(parts[5])
|
|
if err != nil || offset < 0 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid offset"))
|
|
}
|
|
limit, err := strconv.Atoi(parts[6])
|
|
if err != nil || limit < 1 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid limit"))
|
|
}
|
|
if limit > maxChunk {
|
|
limit = maxChunk
|
|
}
|
|
if debug != nil && debug.enabled {
|
|
debug.pullRequests.Add(1)
|
|
debug.chunkf("CPULL id=%s ack=%d want=%d offset=%d limit=%d", parts[2], ack, want, offset, limit)
|
|
}
|
|
|
|
data, total, eof, final, waitExpired, err := s.pull(want, ack, offset, limit, pollWait)
|
|
if err != nil {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
|
}
|
|
if waitExpired {
|
|
if debug != nil && debug.enabled {
|
|
debug.waitRecords.Add(1)
|
|
debug.chunkf("CPULL id=%s want=%d -> WAIT", parts[2], want)
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("WAIT"))
|
|
}
|
|
if eof {
|
|
if debug != nil {
|
|
debug.chunkf("CPULL id=%s want=%d -> EOF final=%d", parts[2], want, final)
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte(fmt.Sprintf("EOF %d", final)))
|
|
}
|
|
|
|
if debug != nil && debug.enabled {
|
|
debug.dataRecords.Add(1)
|
|
debug.chunkf("DATA id=%s seq=%d offset=%d bytes=%d total=%d", parts[2], want, offset, len(data), total)
|
|
}
|
|
prefix := []byte(fmt.Sprintf("DATA %d %d %d ", want, offset, total))
|
|
out := make([]byte, len(prefix)+len(data))
|
|
copy(out, prefix)
|
|
copy(out[len(prefix):], data)
|
|
return protocol.WriteResponseFrame(conn, requestID, out)
|
|
}
|
|
|
|
if bytes.HasPrefix(payload, []byte("CCLOSE ")) {
|
|
parts := strings.Fields(string(payload))
|
|
if len(parts) != 3 {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR bad CCLOSE"))
|
|
}
|
|
if !tokenEqual(decodeWireToken(parts[1]), token) {
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
|
}
|
|
manager.remove(parts[2])
|
|
if debug != nil && debug.enabled {
|
|
debug.sessionsClosed.Add(1)
|
|
debug.activeSessions.Add(-1)
|
|
debug.logf("SESSION CLOSE id=%s peer=%v active_sessions=%d", parts[2], conn.RemoteAddr(), manager.count())
|
|
debug.chunkf("CCLOSE id=%s -> CLOSED", parts[2])
|
|
}
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("CLOSED"))
|
|
}
|
|
|
|
return protocol.WriteResponseFrame(conn, requestID, []byte("ERR unknown chunk command"))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Wire detection and the XOR connection loop.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// prefixedConn replays bytes already consumed for wire detection before falling
|
|
// through to the socket. Using a plain bufio.Reader would be wrong here: the
|
|
// TUNNEL path relays the raw connection, so anything the detector buffered
|
|
// beyond the magic would be lost.
|
|
type prefixedConn struct {
|
|
net.Conn
|
|
r io.Reader
|
|
}
|
|
|
|
func (p *prefixedConn) Read(b []byte) (int, error) { return p.r.Read(b) }
|
|
|
|
// sniffWire reads the two magic bytes and reports whether this connection
|
|
// speaks the legacy XOR framing. The returned conn replays them.
|
|
func sniffWire(conn net.Conn) (net.Conn, bool, error) {
|
|
var magic [2]byte
|
|
if _, err := io.ReadFull(conn, magic[:]); err != nil {
|
|
return conn, false, err
|
|
}
|
|
replayed := &prefixedConn{Conn: conn, r: io.MultiReader(bytes.NewReader(magic[:]), conn)}
|
|
return replayed, magic[0] == 'U' && magic[1] == 'P', nil
|
|
}
|
|
|
|
// handleXOR serves one connection speaking UP/OK + XOR 0xAD: the v4 chunk
|
|
// commands, plus the TUNNEL/TUNNEL2 stream commands.
|
|
func handleXOR(
|
|
conn net.Conn,
|
|
token string,
|
|
allowPrivate bool,
|
|
cache *dnsCache,
|
|
tcpBuffer int,
|
|
manager *chunkManager,
|
|
chunkMax int,
|
|
chunkBuffered int,
|
|
chunkPollWait time.Duration,
|
|
debug *serverDebug,
|
|
) {
|
|
for {
|
|
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
|
|
|
|
requestID, _, payload, err := protocol.ReadRequestFrame(conn)
|
|
if err != nil {
|
|
if debug != nil && debug.enabled && err != io.EOF {
|
|
debug.errorf("peer=%v read XOR request: %v", conn.RemoteAddr(), err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if isChunkCommand(payload) {
|
|
if err := processChunkCommand(
|
|
conn, requestID, payload, token, allowPrivate, cache, tcpBuffer,
|
|
manager, chunkMax, chunkBuffered, chunkPollWait, debug,
|
|
); err != nil {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
|
|
parts := strings.Fields(string(payload))
|
|
transport := "xor"
|
|
if len(parts) == 4 && parts[0] == "TUNNEL" {
|
|
transport = "xor"
|
|
} else if len(parts) == 5 && parts[0] == "TUNNEL2" {
|
|
transport = strings.ToLower(parts[4])
|
|
if transport != "raw" && transport != "xor" {
|
|
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR transport must be RAW or XOR"))
|
|
return
|
|
}
|
|
} else {
|
|
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR expected TUNNEL, TUNNEL2, or chunk command"))
|
|
return
|
|
}
|
|
|
|
if !tokenEqual(parts[1], token) {
|
|
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR authentication failed"))
|
|
return
|
|
}
|
|
|
|
port, err := strconv.Atoi(parts[3])
|
|
if err != nil || port < 1 || port > 65535 {
|
|
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR invalid port"))
|
|
return
|
|
}
|
|
|
|
if debug != nil && debug.enabled {
|
|
debug.logf("TUNNEL peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
target, err := dialTarget(ctx, parts[2], port, allowPrivate, cache, tcpBuffer)
|
|
cancel()
|
|
if err != nil {
|
|
if debug != nil && debug.enabled {
|
|
debug.errorf("TUNNEL target=%s:%d connect failed: %v", parts[2], port, err)
|
|
}
|
|
_ = protocol.WriteResponseFrame(conn, requestID, []byte("ERR "+err.Error()))
|
|
return
|
|
}
|
|
defer target.Close()
|
|
|
|
if err := protocol.WriteResponseFrame(conn, requestID, []byte("CONNECTED")); err != nil {
|
|
return
|
|
}
|
|
|
|
_ = conn.SetDeadline(time.Time{})
|
|
if transport == "raw" {
|
|
protocol.RelayRaw(conn, target)
|
|
} else {
|
|
protocol.RelayXOR(conn, target)
|
|
}
|
|
if debug != nil && debug.enabled {
|
|
debug.logf("TUNNEL closed peer=%v target=%s:%d transport=%s", conn.RemoteAddr(), parts[2], port, transport)
|
|
}
|
|
return
|
|
}
|
|
}
|