From aa713936ddbc193aa83d56bd4256cc9310099dff Mon Sep 17 00:00:00 2001 From: Lonny Wong Date: Sun, 7 Jul 2024 10:37:56 +0800 Subject: [PATCH] support ssh agent forwarding and X11 forwarding over UDP --- go.mod | 18 ++--- go.sum | 36 ++++----- tssh/agent.go | 11 +-- tssh/agent_windows.go | 8 +- tssh/forward.go | 18 +++-- tssh/login.go | 10 ++- tssh/ssh.go | 7 ++ tssh/udp.go | 166 +++++++++++++++++++++++++++++++++++++++++- tssh/xauth.go | 19 +++-- 9 files changed, 241 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index 7c6bd3c..3013224 100644 --- a/go.mod +++ b/go.mod @@ -24,10 +24,10 @@ require ( github.com/trzsz/promptui v0.10.7 github.com/trzsz/ssh_config v1.3.6 github.com/trzsz/trzsz-go v1.1.8-0.20240525015006-6424386a6738 - github.com/trzsz/tsshd v0.1.1 - golang.org/x/crypto v0.24.0 - golang.org/x/sys v0.21.0 - golang.org/x/term v0.21.0 + github.com/trzsz/tsshd v0.1.2 + golang.org/x/crypto v0.25.0 + golang.org/x/sys v0.22.0 + golang.org/x/term v0.22.0 ) require ( @@ -37,7 +37,7 @@ require ( github.com/andybrewer/mack v0.0.0-20220307193339-22e922cc18af // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/boombuler/barcode v1.0.1 // indirect + github.com/boombuler/barcode v1.0.2 // indirect github.com/charmbracelet/x/ansi v0.1.2 // indirect github.com/charmbracelet/x/input v0.1.2 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect @@ -51,13 +51,13 @@ require ( github.com/josephspurrier/goversioninfo v1.4.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/klauspost/reedsolomon v1.12.1 // indirect + github.com/klauspost/reedsolomon v1.12.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect - github.com/ncruces/zenity v0.10.12 // indirect + github.com/ncruces/zenity v0.10.13 // indirect github.com/onsi/ginkgo/v2 v2.19.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -73,8 +73,8 @@ require ( go.uber.org/mock v0.4.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.22.0 // indirect diff --git a/go.sum b/go.sum index c9e134c..c856a31 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= +github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= @@ -87,8 +87,8 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/reedsolomon v1.12.1 h1:NhWgum1efX1x58daOBGCFWcxtEhOhXKKl1HAPQUp03Q= -github.com/klauspost/reedsolomon v1.12.1/go.mod h1:nEi5Kjb6QqtbofI6s+cbG/j1da11c96IBYBSnVGtuBs= +github.com/klauspost/reedsolomon v1.12.2 h1:TC0hlL/tTRxiMNnqHCzKsY11E0fIIKGCoZ2vQoPKIEM= +github.com/klauspost/reedsolomon v1.12.2/go.mod h1:nEi5Kjb6QqtbofI6s+cbG/j1da11c96IBYBSnVGtuBs= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -105,8 +105,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/ncruces/zenity v0.10.12 h1:o4SErDa0kQijlqG6W4OYYzO6kA0fGu34uegvJGcMLBI= -github.com/ncruces/zenity v0.10.12/go.mod h1:5OZIERViRR2fN0FcJCcisqxI+lYMDGzEDCEwB/+8iao= +github.com/ncruces/zenity v0.10.13 h1:0Gd/EdjjEQIhrFaJ05Q5ZvyjlcjnorlZpdzgUzqQIH0= +github.com/ncruces/zenity v0.10.13/go.mod h1:UyAUPSjHm1hOdeZa3Lrh/zmItyGq+iH5AP6xSCx8CFM= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= @@ -155,8 +155,8 @@ github.com/trzsz/ssh_config v1.3.6 h1:+LBCg2uzhAgw2s19yqeUdD4YwW2z4kvlsXtKB6zDjm github.com/trzsz/ssh_config v1.3.6/go.mod h1:uSVHpGOTpBwE1FwyUrtnanlFuxZKt4dvdKFVKe41h58= github.com/trzsz/trzsz-go v1.1.8-0.20240525015006-6424386a6738 h1:yPNjsEKiQAkBtbPBHWKg8sZcx2NhEvK1WmfmVMs3Moc= github.com/trzsz/trzsz-go v1.1.8-0.20240525015006-6424386a6738/go.mod h1:fBEGNZiKmGYodYENKATNuz/U3Wzt6MWWJs3ox0CzPhM= -github.com/trzsz/tsshd v0.1.1 h1:wRVV9Md1yaLzMjD6wKguXSNXlEUyFlPy82g8tfa7sb8= -github.com/trzsz/tsshd v0.1.1/go.mod h1:SIp08r/T0MjihwcdTGWDwHlWwqwxn/DA+40AykfoxbU= +github.com/trzsz/tsshd v0.1.2 h1:LTH2J46GXGpZ65tF21rhLZkpUc7qTFMDd1VdaSzpwHA= +github.com/trzsz/tsshd v0.1.2/go.mod h1:oOk8+QjhNfzAQCnlk64FFBI8qR8/O5f64rpz8zk8CYg= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xtaci/kcp-go/v5 v5.6.8 h1:jlI/0jAyjoOjT/SaGB58s4bQMJiNS41A2RKzR6TMWeI= @@ -172,8 +172,8 @@ go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= @@ -182,16 +182,16 @@ golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -208,10 +208,10 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= diff --git a/tssh/agent.go b/tssh/agent.go index 6e651ba..baed36c 100644 --- a/tssh/agent.go +++ b/tssh/agent.go @@ -39,6 +39,9 @@ var ( agentClient agent.ExtendedAgent ) +type agentRequest struct { +} + func getAgentAddr(args *sshArgs, param *sshParam) (string, error) { if addr := getOptionConfig(args, "IdentityAgent"); addr != "" { if strings.ToLower(addr) == "none" { @@ -85,12 +88,10 @@ func getAgentClient(args *sshArgs, param *sshParam) agent.ExtendedAgent { return agentClient } -const channelType = "auth-agent@openssh.com" - func forwardToRemote(client sshClient, addr string) error { - channels := client.HandleChannelOpen(channelType) + channels := client.HandleChannelOpen(kAgentChannelType) if channels == nil { - return fmt.Errorf("agent: already have handler for %s", channelType) + return fmt.Errorf("agent: already have handler for %s", kAgentChannelType) } conn, err := dialAgent(addr) if err != nil { @@ -122,7 +123,7 @@ func forwardAgentRequest(channel ssh.Channel, addr string) { } func requestAgentForwarding(session sshSession) error { - ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil) + ok, err := session.SendRequest(kAgentRequestName, true, nil) if err != nil { return err } diff --git a/tssh/agent_windows.go b/tssh/agent_windows.go index 64d4009..c365fba 100644 --- a/tssh/agent_windows.go +++ b/tssh/agent_windows.go @@ -26,6 +26,7 @@ package tssh import ( "net" + "strings" "time" "github.com/Microsoft/go-winio" @@ -50,6 +51,9 @@ func dialAgent(addr string) (net.Conn, error) { if addr == kPageantFakeAddr { return pageant.NewPageantConn() } - timeout := time.Second - return winio.DialPipe(addr, &timeout) + if strings.HasPrefix(addr, "\\\\") && strings.Contains(addr, "\\pipe\\") { + timeout := time.Second + return winio.DialPipe(addr, &timeout) + } + return net.DialTimeout("unix", addr, time.Second) } diff --git a/tssh/forward.go b/tssh/forward.go index 3bce8ca..3e74cfc 100644 --- a/tssh/forward.go +++ b/tssh/forward.go @@ -55,6 +55,10 @@ type forwardCfg struct { destPort int } +type closeWriter interface { + CloseWrite() error +} + var spaceRegexp = regexp.MustCompile(`\s+`) var portOnlyRegexp = regexp.MustCompile(`^\d+$`) var ipv6AndPortRegexp = regexp.MustCompile(`^\[([:\da-fA-F]+)\]:(\d+)$`) @@ -489,7 +493,7 @@ func sshX11Forward(args *sshArgs, client sshClient, session sshSession) { AuthCookie: cookie, ScreenNumber: 0, } - ok, err := session.SendRequest("x11-req", true, ssh.Marshal(payload)) + ok, err := session.SendRequest(kX11RequestName, true, ssh.Marshal(payload)) if err != nil { warning("X11 forwarding request failed: %v", err) return @@ -499,9 +503,9 @@ func sshX11Forward(args *sshArgs, client sshClient, session sshSession) { return } - channels := client.HandleChannelOpen("x11") + channels := client.HandleChannelOpen(kX11ChannelType) if channels == nil { - warning("already have handler for x11") + warning("already have handler for %s", kX11ChannelType) return } go func() { @@ -589,8 +593,12 @@ func forwardChannel(channel ssh.Channel, conn net.Conn) { wg.Add(2) go func() { _, _ = io.Copy(conn, channel) - if unixConn, ok := conn.(*net.UnixConn); ok { - _ = unixConn.CloseWrite() + if cw, ok := conn.(closeWriter); ok { + _ = cw.CloseWrite() + } else { + // close the entire stream since there is no half-close + time.Sleep(200 * time.Millisecond) + _ = conn.Close() } wg.Done() }() diff --git a/tssh/login.go b/tssh/login.go index cd354e0..19f469b 100644 --- a/tssh/login.go +++ b/tssh/login.go @@ -1232,7 +1232,6 @@ func sshTcpLogin(args *sshArgs) (ss *sshClientSession, param *sshParam, udpMode if !control && udpMode == kUdpModeNo { // ssh agent forward sshAgentForward(args, param, ss.client, ss.session) - // x11 forward sshX11Forward(args, ss.client, ss.session) } @@ -1259,6 +1258,15 @@ func sshLogin(args *sshArgs) (*sshClientSession, error) { return nil, err } } + + // ssh agent forward and x11 forward + // if not running as a proxy ( aka: not stdio forward ) and executing remote command + if args.StdioForward == "" && !args.NoCommand { + // ssh agent forward + sshAgentForward(args, param, ss.client, ss.session) + // x11 forward + sshX11Forward(args, ss.client, ss.session) + } } // if running as a proxy ( aka: stdio forward ), or if not executing remote command, diff --git a/tssh/ssh.go b/tssh/ssh.go index 55553f4..0131e90 100644 --- a/tssh/ssh.go +++ b/tssh/ssh.go @@ -33,6 +33,13 @@ import ( "golang.org/x/crypto/ssh" ) +const ( + kX11ChannelType = "x11" + kX11RequestName = "x11-req" + kAgentChannelType = "auth-agent@openssh.com" + kAgentRequestName = "auth-agent-req@openssh.com" +) + type sshClient interface { Wait() error Close() error diff --git a/tssh/udp.go b/tssh/udp.go index bc3ecc5..dfec9b6 100644 --- a/tssh/udp.go +++ b/tssh/udp.go @@ -57,6 +57,8 @@ type sshUdpClient struct { sessionMutex sync.Mutex sessionID atomic.Uint64 sessionMap map[uint64]*sshUdpSession + channelMutex sync.Mutex + channelMap map[string]chan ssh.NewChannel lastAliveTime atomic.Pointer[time.Time] closed atomic.Bool } @@ -114,6 +116,7 @@ func (c *sshUdpClient) Close() error { warning("send close command failed: %v", err) } c.busStream.Close() + time.Sleep(500 * time.Millisecond) // give udp some time done <- struct{}{} }() @@ -158,7 +161,7 @@ func (c *sshUdpClient) DialTimeout(network, addr string, timeout time.Duration) return nil, err } c.wg.Add(1) - return &sshUdpConn{stream, c}, nil + return &sshUdpConn{Conn: stream, client: c}, nil } func (c *sshUdpClient) Listen(network, addr string) (net.Listener, error) { @@ -183,7 +186,20 @@ func (c *sshUdpClient) Listen(network, addr string) (net.Listener, error) { } func (c *sshUdpClient) HandleChannelOpen(channelType string) <-chan ssh.NewChannel { - return nil + c.channelMutex.Lock() + defer c.channelMutex.Unlock() + if _, ok := c.channelMap[channelType]; ok { + return nil + } + switch channelType { + case kAgentChannelType, kX11ChannelType: + ch := make(chan ssh.NewChannel) + c.channelMap[channelType] = ch + return ch + default: + warning("channel type [%s] is not supported yet", channelType) + return nil + } } func (c *sshUdpClient) SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) { @@ -249,6 +265,8 @@ func (c *sshUdpClient) handleBusEvent() { c.handleExitEvent() case "error": c.handleErrorEvent() + case "channel": + c.handleChannelEvent() case "alive": now := time.Now() c.lastAliveTime.Store(&now) @@ -288,6 +306,26 @@ func (c *sshUdpClient) handleErrorEvent() { warning("udp error: %s", errMsg.Msg) } +func (c *sshUdpClient) handleChannelEvent() { + var channelMsg tsshd.ChannelMessage + if err := tsshd.RecvMessage(c.busStream, &channelMsg); err != nil { + warning("recv channel message failed: %v", err) + return + } + c.channelMutex.Lock() + defer c.channelMutex.Unlock() + if ch, ok := c.channelMap[channelMsg.ChannelType]; ok { + go func() { + ch <- &sshUdpNewChannel{ + client: c, + channelType: channelMsg.ChannelType, + id: channelMsg.ID} + }() + } else { + warning("channel [%s] has no handler", channelMsg.ChannelType) + } +} + func (c *sshUdpClient) exit(code int) { c.sessionMutex.Lock() defer c.sessionMutex.Unlock() @@ -308,10 +346,13 @@ type sshUdpSession struct { width int envs map[string]string started bool + closed bool stdin io.Reader stdout io.WriteCloser stderr net.Conn code int + x11 *x11Request + agent *agentRequest } func (s *sshUdpSession) Wait() error { @@ -323,6 +364,10 @@ func (s *sshUdpSession) Wait() error { } func (s *sshUdpSession) Close() error { + if s.closed { + return nil + } + s.closed = true if s.stdout != nil { _ = s.stdout.Close() } @@ -375,6 +420,20 @@ func (s *sshUdpSession) startSession(msg *tsshd.StartMessage) error { return fmt.Errorf("session already started") } s.started = true + if s.x11 != nil { + msg.X11 = &tsshd.X11Request{ + ChannelType: kX11ChannelType, + SingleConnection: s.x11.SingleConnection, + AuthProtocol: s.x11.AuthProtocol, + AuthCookie: s.x11.AuthCookie, + ScreenNumber: s.x11.ScreenNumber, + } + } + if s.agent != nil { + msg.Agent = &tsshd.AgentRequest{ + ChannelType: kAgentChannelType, + } + } if err := tsshd.SendMessage(s.stream, msg); err != nil { return fmt.Errorf("send session message failed: %v", err) } @@ -499,12 +558,32 @@ func (s *sshUdpSession) RequestPty(term string, height, width int, termmodes ssh } func (s *sshUdpSession) SendRequest(name string, wantReply bool, payload []byte) (bool, error) { - return false, fmt.Errorf("ssh udp session SendRequest is not supported yet") + switch name { + case kX11RequestName: + s.x11 = &x11Request{} + if payload != nil { + if err := ssh.Unmarshal(payload, s.x11); err != nil { + return false, fmt.Errorf("unmarshal x11 request failed: %v", err) + } + } + return true, nil + case kAgentRequestName: + s.agent = &agentRequest{} + if payload != nil { + if err := ssh.Unmarshal(payload, s.agent); err != nil { + return false, fmt.Errorf("unmarshal agent request failed: %v", err) + } + } + return true, nil + default: + return false, fmt.Errorf("ssh udp session SendRequest [%s] is not supported yet", name) + } } type sshUdpListener struct { client *sshUdpClient stream net.Conn + closed bool } func (l *sshUdpListener) Accept() (net.Conn, error) { @@ -525,10 +604,14 @@ func (l *sshUdpListener) Accept() (net.Conn, error) { return nil, err } l.client.wg.Add(1) - return &sshUdpConn{stream, l.client}, nil + return &sshUdpConn{Conn: stream, client: l.client}, nil } func (l *sshUdpListener) Close() error { + if l.closed { + return nil + } + l.closed = true l.client.wg.Done() return l.stream.Close() } @@ -540,13 +623,87 @@ func (l *sshUdpListener) Addr() net.Addr { type sshUdpConn struct { net.Conn client *sshUdpClient + closed bool } func (c *sshUdpConn) Close() error { + if c.closed { + return nil + } + c.closed = true c.client.wg.Done() return c.Conn.Close() } +type sshUdpNewChannel struct { + client *sshUdpClient + channelType string + id uint64 +} + +func (c *sshUdpNewChannel) Accept() (ssh.Channel, <-chan *ssh.Request, error) { + stream, err := c.client.newStream("accept") + if err != nil { + return nil, nil, err + } + if err := tsshd.SendMessage(stream, &tsshd.AcceptMessage{ID: c.id}); err != nil { + stream.Close() + return nil, nil, fmt.Errorf("send accept message failed: %v", err) + } + if err := tsshd.RecvError(stream); err != nil { + stream.Close() + return nil, nil, err + } + c.client.wg.Add(1) + return &sshUdpChannel{Conn: stream, client: c.client}, nil, nil +} + +func (c *sshUdpNewChannel) Reject(reason ssh.RejectionReason, message string) error { + return fmt.Errorf("ssh udp new channel Reject is not supported yet") +} + +func (c *sshUdpNewChannel) ChannelType() string { + return c.channelType +} + +func (c *sshUdpNewChannel) ExtraData() []byte { + return nil +} + +type sshUdpChannel struct { + net.Conn + client *sshUdpClient + closed bool +} + +func (c *sshUdpChannel) Close() error { + if c.closed { + return nil + } + c.closed = true + c.client.wg.Done() + return c.Conn.Close() +} + +func (c *sshUdpChannel) CloseWrite() error { + if cw, ok := c.Conn.(closeWriter); ok { + return cw.CloseWrite() + } else { + // close the entire stream since there is no half-close + time.Sleep(200 * time.Millisecond) + return c.Close() + } +} + +func (c *sshUdpChannel) SendRequest(name string, wantReply bool, payload []byte) (bool, error) { + return false, fmt.Errorf("ssh udp channel SendRequest is not supported yet") +} + +func (c *sshUdpChannel) Stderr() io.ReadWriter { + warning("ssh udp channel Stderr is not supported yet") + return nil +} + func sshUdpLogin(args *sshArgs, param *sshParam, ss *sshClientSession, udpMode int) (*sshClientSession, error) { defer ss.Close() @@ -562,6 +719,7 @@ func sshUdpLogin(args *sshArgs, param *sshParam, ss *sshClientSession, udpMode i udpClient := sshUdpClient{ client: client, sessionMap: make(map[uint64]*sshUdpSession), + channelMap: make(map[string]chan ssh.NewChannel), } busStream, err := udpClient.newStream("bus") diff --git a/tssh/xauth.go b/tssh/xauth.go index dee9547..f43af56 100644 --- a/tssh/xauth.go +++ b/tssh/xauth.go @@ -50,7 +50,8 @@ func getXauthAndProto(display string, trusted bool, timeout int) (string, string if !trusted { file, err := os.CreateTemp("", "xauthfile_*") if err != nil { - return "", "", fmt.Errorf("create xauth file failed: %v", err) + debug("create xauth file failed: %v", err) + return genFakeXauth(trusted) } path := file.Name() defer os.Remove(path) @@ -60,7 +61,8 @@ func getXauthAndProto(display string, trusted bool, timeout int) (string, string } debug("xauth generate command: %v", genArgs) if _, err := execXauthCommand(genArgs); err != nil { - return "", "", fmt.Errorf("xauth generate failed: %v", err) + debug("xauth generate failed: %v", err) + return genFakeXauth(trusted) } listArgs = []string{"-f", path, "list", display} } else { @@ -70,12 +72,14 @@ func getXauthAndProto(display string, trusted bool, timeout int) (string, string debug("xauth list command: %v", listArgs) out, err := execXauthCommand(listArgs) if err != nil { - return "", "", fmt.Errorf("xauth list failed: %v", err) + debug("xauth list failed: %v", err) + return genFakeXauth(trusted) } if out != "" { tokens := strings.Fields(out) if len(tokens) < 3 { - return "", "", fmt.Errorf("invalid xauth list output: %s", out) + debug("invalid xauth list output: %s", out) + return genFakeXauth(trusted) } return tokens[2], tokens[1], nil } @@ -98,13 +102,12 @@ func execXauthCommand(args []string) (string, error) { } func genFakeXauth(trusted bool) (string, string, error) { - if !trusted { - return "", "", fmt.Errorf("untrusted X11 forwarding setup failed since no xauth program") - } cookie := make([]byte, 16) if _, err := rand.Read(cookie); err != nil { return "", "", fmt.Errorf("random cookie failed: %v", err) } - warning("No xauth data; using fake authentication data for X11 forwarding.") + if trusted { + warning("No xauth data; using fake authentication data for X11 forwarding.") + } return fmt.Sprintf("%x", cookie), kSshX11Proto, nil }