update: use raw TCP for HTTP server

This commit is contained in:
2025-04-02 23:27:39 +07:00
parent 58f15d5a67
commit 221adf9581
8 changed files with 232 additions and 545 deletions

View File

@ -1,179 +0,0 @@
package httpServer
import (
"bufio"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"strings"
"tunnel_pls/session"
"tunnel_pls/utils"
indexView "tunnel_pls/view/index"
)
type RouteHandler http.HandlerFunc
// Simple Router Struct
type Router struct {
routes map[string]map[string]http.Handler
}
// NewRouter initializes a new router
func NewRouter() *Router {
return &Router{
routes: make(map[string]map[string]http.Handler),
}
}
// Handle registers a route with an http.Handler
func (r *Router) Handle(method, path string, handler http.Handler) {
if _, exists := r.routes[method]; !exists {
r.routes[method] = make(map[string]http.Handler)
}
r.routes[method][path] = handler
}
// HandleFunc registers a route with a function
func (r *Router) HandleFunc(method, path string, handlerFunc func(http.ResponseWriter, *http.Request)) {
r.Handle(method, path, http.HandlerFunc(handlerFunc))
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if methodRoutes, exists := r.routes[req.Method]; exists {
if handler, exists := methodRoutes[req.URL.Path]; exists {
handler.ServeHTTP(w, req)
return
}
}
http.Error(w, "404 Not Found", http.StatusNotFound)
}
type tcpResponseWriter struct {
conn net.Conn
header http.Header
status int
}
func (w *tcpResponseWriter) Header() http.Header {
return w.header
}
func (w *tcpResponseWriter) WriteHeader(statusCode int) {
w.status = statusCode
}
func (w *tcpResponseWriter) Write(data []byte) (int, error) {
fmt.Println("here")
resp := fmt.Sprintf("HTTP/1.1 %d %s\r\n", w.status, http.StatusText(w.status))
for k, v := range w.header {
resp += fmt.Sprintf("%s: %s\r\n", k, v[0])
}
resp += "\r\n" + string(data)
return w.conn.Write([]byte(resp))
}
var router = NewRouter()
func Listen() {
server, err := net.Listen("tcp", ":80")
if err != nil {
log.Fatal(err)
return
}
router.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) {
indexView.Main("Main Page", utils.Getenv("domain")).Render(r.Context(), w)
return
})
router.HandleFunc("GET", "/public/output.css", func(w http.ResponseWriter, r *http.Request) {
open, err := os.Open("public/output.css")
if err != nil {
return
}
data, _ := io.ReadAll(open)
fmt.Fprintf(w, string(data))
})
//fileserver := http.FileServer(http.Dir("./public"))
//router.Handle("/public/", http.StripPrefix("/public", fileserver))
defer server.Close()
log.Println("Listening on :80")
for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
return
}
go handleRequest(conn)
}
}
func handleRequest(conn net.Conn) {
defer conn.Close()
var rawRequest string
reader := bufio.NewReader(conn)
r, err := http.ReadRequest(reader)
if err != nil {
fmt.Println("Error reading request:", err)
return
}
writer := &tcpResponseWriter{
conn: conn,
header: make(http.Header),
status: http.StatusOK,
}
if r.Host == utils.Getenv("domain") {
router.ServeHTTP(writer, r)
return
}
if utils.Getenv("tls_enabled") == "false" {
http.Redirect(writer, r, fmt.Sprintf("https://%s%s", r.Host, r.URL.RequestURI()), http.StatusFound)
return
}
slug := strings.Split(r.Host, ".")[0]
if slug == "" {
fmt.Println("Error parsing slug: ", r.Host)
return
}
sshSession, ok := session.Clients[slug]
if !ok {
fmt.Println("Error finding ssh session: ", slug)
return
}
rawRequest += fmt.Sprintf("%s %s %s\r\n", r.Method, r.URL.RequestURI(), r.Proto)
rawRequest += fmt.Sprintf("Host: %s\r\n", r.Host)
for k, v := range r.Header {
rawRequest += fmt.Sprintf("%s: %s\r\n", k, v[0])
}
rawRequest += "\r\n"
if r.Body != nil {
body, err := io.ReadAll(r.Body)
if err != nil {
log.Println("Error reading request body:", err)
} else {
rawRequest += string(body)
}
}
payload := []byte(rawRequest)
host, originPort := session.ParseAddr(conn.RemoteAddr().String())
sshSession.GetForwardedConnection(conn, host, sshSession.Connection, payload, originPort, 80, r.RequestURI, r.Method, r.Proto)
}

View File

@ -1,180 +0,0 @@
package httpServer
import (
"crypto/tls"
"fmt"
"golang.org/x/net/http2"
"io"
"log"
"net/http"
"os"
"strings"
"tunnel_pls/session"
"tunnel_pls/utils"
indexView "tunnel_pls/view/index"
)
func ListenTLS(config *tls.Config) {
server := &http.Server{
Addr: ":443",
TLSConfig: config,
Handler: http.HandlerFunc(handleRequestTLS),
}
http2.ConfigureServer(server, &http2.Server{})
fmt.Println("Listening on :8443 (HTTP/2 over TLS)")
log.Fatal(server.ListenAndServeTLS("", ""))
}
func handleRequestTLS(w http.ResponseWriter, r *http.Request) {
_, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var rawRequest string
if r.Host == utils.Getenv("domain") {
TLSRouter().ServeHTTP(w, r)
return
}
slug := strings.Split(r.Host, ".")[0]
if slug == "" {
fmt.Println("Error parsing slug: ", r.Host)
return
}
sshSession, ok := session.Clients[slug]
if !ok {
fmt.Println("Error finding ssh session: ", slug)
http.Error(w, "No tunnel found on this address", http.StatusNotFound)
return
}
rawRequest += fmt.Sprintf("%s %s %s\r\n", r.Method, r.URL.RequestURI(), "HTTP/1.1")
rawRequest += fmt.Sprintf("Host: %s\r\n", r.Host)
for k, v := range r.Header {
rawRequest += fmt.Sprintf("%s: %s\r\n", k, v[0])
}
rawRequest += "\r\n"
if r.Body != nil {
body, err := io.ReadAll(r.Body)
if err != nil {
log.Println("Error reading request body:", err)
} else {
rawRequest += string(body)
}
}
payload := []byte(rawRequest)
host, originPort := session.ParseAddr(r.RemoteAddr)
response := sshSession.GetForwardedConnectionTLS(host, sshSession.Connection, payload, originPort, 80, r.RequestURI, r.Method, r.Proto)
forbiddenHeaders := map[string]bool{
"connection": true,
"transfer-encoding": true,
"upgrade": true,
"keep-alive": true,
}
for k, v := range response.Header {
k = strings.ToLower(k)
if forbiddenHeaders[k] {
continue
}
if k == ":status" || k == ":method" || k == ":path" || k == ":authority" {
continue
}
w.Header().Set(k, v[0])
}
io.Copy(w, response.Body)
return
}
func TLSRouter() *http.ServeMux {
handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
indexView.Main("Main Page", utils.Getenv("domain")).Render(r.Context(), w)
return
})
handler.HandleFunc("/public/output.css", func(w http.ResponseWriter, r *http.Request) {
open, err := os.Open("public/output.css")
if err != nil {
return
}
defer open.Close()
w.Header().Set("Content-Type", "text/css; charset=utf-8")
io.Copy(w, open)
return
})
return handler
}
//func handleRequestTLS(conn net.Conn) {
// defer conn.Close()
// var rawRequest string
//
// reader := bufio.NewReader(conn)
// r, err := http.ReadRequest(reader)
// if err != nil {
// fmt.Println("Error reading request:", err)
// return
// }
//
// writer := &tcpResponseWriter{
// conn: conn,
// header: make(http.Header),
// status: http.StatusOK,
// }
//
// if r.Host == utils.Getenv("domain") {
// router.ServeHTTP(writer, r)
// return
// }
//
// slug := strings.Split(r.Host, ".")[0]
// if slug == "" {
// fmt.Println("Error parsing slug: ", r.Host)
// return
// }
//
// sshSession, ok := session.Clients[slug]
// if !ok {
// fmt.Println("Error finding ssh session: ", slug)
// return
// }
//
// rawRequest += fmt.Sprintf("%s %s %s\r\n", r.Method, r.URL.RequestURI(), r.Proto)
// rawRequest += fmt.Sprintf("Host: %s\r\n", r.Host)
//
// for k, v := range r.Header {
// rawRequest += fmt.Sprintf("%s: %s\r\n", k, v[0])
// }
// rawRequest += "\r\n"
//
// if r.Body != nil {
// body, err := io.ReadAll(r.Body)
// if err != nil {
// log.Println("Error reading request body:", err)
// } else {
// rawRequest += string(body)
// }
// }
//
// payload := []byte(rawRequest)
//
// host, originPort := session.ParseAddr(conn.RemoteAddr().String())
// sshSession.GetForwardedConnection(conn, host, sshSession.Connection, payload, originPort, 80, r.RequestURI, r.Method, r.Proto)
//}

BIN
main

Binary file not shown.

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

63
server/http.go Normal file
View File

@ -0,0 +1,63 @@
package server
import (
"bufio"
"errors"
"fmt"
"log"
"net"
"net/http"
"strings"
"tunnel_pls/session"
)
func NewHTTPServer() error {
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:80"))
if err != nil {
return errors.New("Error listening: " + err.Error())
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
log.Printf("Error accepting connection: %v", err)
continue
}
go Handler(conn)
}
}()
return nil
}
func Handler(conn net.Conn) {
reader := bufio.NewReader(conn)
request, err := http.ReadRequest(reader)
if err != nil {
fmt.Println("Error reading request:", err)
return
}
host := strings.Split(request.Host, ".")
if len(host) < 1 {
conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
conn.Close()
return
}
slug := host[0]
sshSession, ok := session.Clients[slug]
if !ok {
conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
conn.Close()
return
}
request.Header.Set("Connection", "keep-alive")
request.Header.Set("Keep-Alive", "timeout=60")
go sshSession.HandleForwardedConnectionHTTP(conn, sshSession.Connection, request)
}

View File

@ -1,14 +1,11 @@
package server package server
import ( import (
"crypto/tls"
"fmt" "fmt"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"log" "log"
"net" "net"
"net/http" "net/http"
httpServer "tunnel_pls/http"
"tunnel_pls/utils"
) )
type Server struct { type Server struct {
@ -23,16 +20,12 @@ func NewServer(config ssh.ServerConfig) *Server {
log.Fatalf("failed to listen on port 2200: %v", err) log.Fatalf("failed to listen on port 2200: %v", err)
return nil return nil
} }
go httpServer.Listen() go func() {
if utils.Getenv("tls_enabled") == "true" { err := NewHTTPServer()
cert, err := tls.LoadX509KeyPair(utils.Getenv("cert_loc"), utils.Getenv("key_loc"))
if err != nil { if err != nil {
log.Fatal("Failed to load key pair:", err) log.Fatalf("failed to start http server: %v", err)
}
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}, NextProtos: []string{"h2"}}
go httpServer.ListenTLS(tlsConfig)
} }
}()
return &Server{ return &Server{
Conn: &listener, Conn: &listener,
Config: &config, Config: &config,

View File

@ -13,10 +13,14 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"tunnel_pls/proto"
"tunnel_pls/utils" "tunnel_pls/utils"
) )
type UserConnection struct {
Reader io.Reader
Writer net.Conn
}
func (s *Session) handleGlobalRequest() { func (s *Session) handleGlobalRequest() {
for { for {
select { select {
@ -25,6 +29,18 @@ func (s *Session) handleGlobalRequest() {
return return
} }
if req.Type == "tcpip-forward" { if req.Type == "tcpip-forward" {
s.handleTCPIPForward(req)
continue
} else {
req.Reply(false, nil)
}
case <-s.Done:
break
}
}
}
func (s *Session) handleTCPIPForward(req *ssh.Request) {
log.Println("Port forwarding request detected") log.Println("Port forwarding request detected")
reader := bytes.NewReader(req.Payload) reader := bytes.NewReader(req.Payload)
@ -33,7 +49,7 @@ func (s *Session) handleGlobalRequest() {
if err != nil { if err != nil {
log.Println("Failed to read address from payload:", err) log.Println("Failed to read address from payload:", err)
req.Reply(false, nil) req.Reply(false, nil)
continue return
} }
var portToBind uint32 var portToBind uint32
@ -41,7 +57,7 @@ func (s *Session) handleGlobalRequest() {
if err := binary.Read(reader, binary.BigEndian, &portToBind); err != nil { if err := binary.Read(reader, binary.BigEndian, &portToBind); err != nil {
log.Println("Failed to read port from payload:", err) log.Println("Failed to read port from payload:", err)
req.Reply(false, nil) req.Reply(false, nil)
continue return
} }
if portToBind == 80 || portToBind == 443 { if portToBind == 80 || portToBind == 443 {
@ -51,21 +67,22 @@ func (s *Session) handleGlobalRequest() {
for { for {
slug = utils.GenerateRandomString(32) slug = utils.GenerateRandomString(32)
if _, ok := Clients[slug]; ok { if _, ok := Clients[slug]; ok {
continue return
} }
break break
} }
Clients[slug] = s Clients[slug] = s
s.Slug = slug s.Slug = slug
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint32(portToBind)) binary.Write(buf, binary.BigEndian, uint32(80))
log.Printf("Forwarding approved on port: %d", portToBind) log.Printf("Forwarding approved on port: %d", 80)
if utils.Getenv("tls_enabled") == "true" { if utils.Getenv("tls_enabled") == "true" {
s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to https://%s.%s \r\n", slug, utils.Getenv("domain")))) s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to https://%s.%s \r\n", slug, utils.Getenv("domain"))))
} else { } else {
s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to http://%s.%s \r\n", slug, utils.Getenv("domain")))) s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to http://%s.%s \r\n", slug, utils.Getenv("domain"))))
} }
req.Reply(true, buf.Bytes()) req.Reply(true, buf.Bytes())
} else { } else {
s.TunnelType = TCP s.TunnelType = TCP
log.Printf("Requested forwarding on %s:%d", addr, portToBind) log.Printf("Requested forwarding on %s:%d", addr, portToBind)
@ -74,7 +91,7 @@ func (s *Session) handleGlobalRequest() {
if err != nil { if err != nil {
log.Printf("Failed to bind to port %d: %v", portToBind, err) log.Printf("Failed to bind to port %d: %v", portToBind, err)
req.Reply(false, nil) req.Reply(false, nil)
continue return
} }
s.Listener = listener s.Listener = listener
s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to %s:%d \r\n", utils.Getenv("domain"), portToBind))) s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to %s:%d \r\n", utils.Getenv("domain"), portToBind)))
@ -89,7 +106,10 @@ func (s *Session) handleGlobalRequest() {
continue continue
} }
go s.HandleForwardedConnection(conn, s.Connection, portToBind) go s.HandleForwardedConnection(UserConnection{
Reader: nil,
Writer: conn,
}, s.Connection, portToBind)
} }
}() }()
@ -100,13 +120,6 @@ func (s *Session) handleGlobalRequest() {
req.Reply(true, buf.Bytes()) req.Reply(true, buf.Bytes())
} }
} else {
req.Reply(false, nil)
}
case <-s.Done:
break
}
}
} }
func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) { func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) {
@ -213,70 +226,53 @@ func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) {
}() }()
} }
func (s *Session) HandleForwardedConnection(conn net.Conn, sshConn *ssh.ServerConn, port uint32) { func (s *Session) HandleForwardedConnection(conn UserConnection, sshConn *ssh.ServerConn, port uint32) {
defer conn.Close() defer conn.Writer.Close()
log.Printf("Handling new forwarded connection from %s", conn.RemoteAddr()) log.Printf("Handling new forwarded connection from %s", conn.Writer.RemoteAddr())
host, originPort := ParseAddr(conn.RemoteAddr().String()) host, originPort := ParseAddr(conn.Writer.RemoteAddr().String())
payload := createForwardedTCPIPPayload(host, originPort, port) payload := createForwardedTCPIPPayload(host, originPort, port)
channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", payload) channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", payload)
if err != nil {
log.Printf("Failed to open forwarded-tcpip channel: %v", err)
return
}
defer channel.Close()
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
connReader := bufio.NewReader(conn)
var isHttp bool
header, err := connReader.Peek(7)
if err != nil {
isHttp = false
} else {
isHttp = proto.IsHttpRequest(header)
}
conn.SetReadDeadline(time.Time{})
go io.Copy(channel, connReader)
reader := bufio.NewReader(channel)
_, err = reader.Peek(1)
if err == io.EOF {
if isHttp {
io.Copy(conn, bytes.NewReader([]byte("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 11\r\nContent-Type: text/plain\r\n\r\nBad Gateway")))
} else {
conn.Write([]byte("Could not forward request to the tunnel addr\r\n"))
}
s.ConnChannels[0].Write([]byte("Could not forward request to the tunnel addr\r\n"))
return
} else {
io.Copy(conn, reader)
}
go func() { go func() {
for req := range reqs { for req := range reqs {
req.Reply(false, nil) req.Reply(false, nil)
} }
}() }()
if err != nil {
log.Printf("Failed to open forwarded-tcpip channel: %v", err)
return
}
defer channel.Close()
if conn.Reader == nil {
conn.Reader = bufio.NewReader(conn.Writer)
}
go io.Copy(channel, conn.Reader)
reader := bufio.NewReader(channel)
_, err = reader.Peek(1)
if err == io.EOF {
fmt.Println("error babi")
}
io.Copy(conn.Writer, reader)
} }
func (s *Session) GetForwardedConnection(conn net.Conn, host string, sshConn *ssh.ServerConn, payload []byte, originPort, port uint32, path, method, proto string) { func (s *Session) HandleForwardedConnectionHTTP(conn net.Conn, sshConn *ssh.ServerConn, request *http.Request) {
defer conn.Close() defer conn.Close()
channelPayload := createForwardedTCPIPPayload(host, originPort, port) fmt.Println(request)
channelPayload := createForwardedTCPIPPayload(request.Host, 80, 80)
channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", channelPayload) channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", channelPayload)
if err != nil { go func() {
log.Printf("Failed to open forwarded-tcpip channel: %v", err) for req := range reqs {
req.Reply(false, nil)
}
}()
var requestBuffer bytes.Buffer
if err := request.Write(&requestBuffer); err != nil {
fmt.Println("Error serializing request:", err)
channel.Close()
conn.Close()
return return
} }
defer channel.Close() channel.Write(requestBuffer.Bytes())
connReader := bufio.NewReader(conn)
initalPayload := bytes.NewReader(payload)
io.Copy(channel, initalPayload)
go io.Copy(channel, connReader)
reader := bufio.NewReader(channel) reader := bufio.NewReader(channel)
_, err = reader.Peek(1) _, err = reader.Peek(1)
@ -285,46 +281,42 @@ func (s *Session) GetForwardedConnection(conn net.Conn, host string, sshConn *ss
s.ConnChannels[0].Write([]byte("Could not forward request to the tunnel addr\r\n")) s.ConnChannels[0].Write([]byte("Could not forward request to the tunnel addr\r\n"))
return return
} else { } else {
s.ConnChannels[0].Write([]byte(fmt.Sprintf("\033[32m %s -- [%s] \"%s %s %s\" \r\n \033[0m", host, time.Now().Format("02/Jan/2006 15:04:05"), method, path, proto))) s.ConnChannels[0].Write([]byte(fmt.Sprintf("\033[32m %s -- [%s] \"%s %s %s\" \r\n \033[0m", request.Host, time.Now().Format("02/Jan/2006 15:04:05"), request.Method, request.RequestURI, request.Proto)))
io.Copy(conn, reader) io.Copy(conn, reader)
} }
go func() {
for req := range reqs {
req.Reply(false, nil)
}
}()
} }
func (s *Session) GetForwardedConnectionTLS(host string, sshConn *ssh.ServerConn, payload []byte, originPort, port uint32, path, method, proto string) *http.Response { //TODO: Implement HTTPS forwarding
channelPayload := createForwardedTCPIPPayload(host, originPort, port) //func (s *Session) GetForwardedConnectionTLS(host string, sshConn *ssh.ServerConn, payload []byte, originPort, port uint32, path, method, proto string) (*http.Response, error) {
channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", channelPayload) // channelPayload := createForwardedTCPIPPayload(host, originPort, port)
if err != nil { // channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", channelPayload)
log.Printf("Failed to open forwarded-tcpip channel: %v", err) // if err != nil {
return nil // return nil, err
} // }
defer channel.Close() // defer channel.Close()
//
initalPayload := bytes.NewReader(payload) // initalPayload := bytes.NewReader(payload)
io.Copy(channel, initalPayload) // io.Copy(channel, initalPayload)
//
go func() { // go func() {
for req := range reqs { // for req := range reqs {
req.Reply(false, nil) // req.Reply(false, nil)
} // }
}() // }()
//
reader := bufio.NewReader(channel) // reader := bufio.NewReader(channel)
_, err = reader.Peek(1) // _, err = reader.Peek(1)
if err == io.EOF { // if err == io.EOF {
s.ConnChannels[0].Write([]byte("Could not forward request to the tunnel addr\r\n")) // return nil, err
return nil // } else {
} else { // s.ConnChannels[0].Write([]byte(fmt.Sprintf("\033[32m %s -- [%s] \"%s %s %s\" \r\n \033[0m", host, time.Now().Format("02/Jan/2006 15:04:05"), method, path, proto)))
s.ConnChannels[0].Write([]byte(fmt.Sprintf("\033[32m %s -- [%s] \"%s %s %s\" \r\n \033[0m", host, time.Now().Format("02/Jan/2006 15:04:05"), method, path, proto))) // response, err := http.ReadResponse(reader, nil)
response, _ := http.ReadResponse(reader, nil) // if err != nil {
return response // return nil, err
} // }
// return response, err
} // }
//}
func writeSSHString(buffer *bytes.Buffer, str string) { func writeSSHString(buffer *bytes.Buffer, str string) {
binary.Write(buffer, binary.BigEndian, uint32(len(str))) binary.Write(buffer, binary.BigEndian, uint32(len(str)))

View File

@ -17,6 +17,7 @@ type Session struct {
TunnelType TunnelType TunnelType TunnelType
ForwardedPort uint16 ForwardedPort uint16
Done chan bool Done chan bool
ForwardedChannel ssh.Channel
} }
type TunnelType string type TunnelType string