Skip to content

Commit

Permalink
support X11 forwarding #97
Browse files Browse the repository at this point in the history
ForwardX11
ForwardX11Trusted
ForwardX11Timeout
  • Loading branch information
lonnywong committed May 5, 2024
1 parent 1e6121f commit 3066b3c
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 27 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ trzsz-ssh ( tssh ) works exactly like the openssh client. The following common f
| Multiplexing | `ControlMaster` `ControlPath` `ControlPersist` |
| Command | `RemoteCommand`, `LocalCommand`, `PermitLocalCommand` |
| SSH Agent | `-a` `-A` `ForwardAgent` `IdentityAgent` `SSH_AUTH_SOCK` |
| X11 Forward | `-x` `-X` `-Y` `ForwardX11` `ForwardX11Trusted` `ForwardX11Timeout` |
| Known Hosts | `UserKnownHostsFile` `GlobalKnownHostsFile` `StrictHostKeyChecking` |
| Basic Login | `-l` `-p` `-i` `-F` `HostName` `Port` `User` `IdentityFile` `SendEnv` `SetEnv` |
| Authentication | `PubkeyAuthentication` `PasswordAuthentication` `KbdInteractiveAuthentication` |
Expand Down
26 changes: 4 additions & 22 deletions tssh/agent.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
package tssh

/*
MIT License
Expand All @@ -24,10 +22,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package tssh

import (
"fmt"
"io"
"net"
"os"
"strings"
"sync"
Expand Down Expand Up @@ -119,25 +117,9 @@ func forwardToRemote(client *ssh.Client, addr string) error {
func forwardAgentRequest(channel ssh.Channel, addr string) {
conn, err := dialAgent(addr)
if err != nil {
debug("ssh agent dial [%s] failed: %v", addr, err)
return
}

var wg sync.WaitGroup
wg.Add(2)
go func() {
_, _ = io.Copy(conn, channel)
if unixConn, ok := conn.(*net.UnixConn); ok {
_ = unixConn.CloseWrite()
}
wg.Done()
}()
go func() {
_, _ = io.Copy(channel, conn)
_ = channel.CloseWrite()
wg.Done()
}()

wg.Wait()
conn.Close()
channel.Close()
forwardChannel(channel, conn)
}
4 changes: 2 additions & 2 deletions tssh/agent_windows.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
package tssh

/*
MIT License
Expand All @@ -24,6 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package tssh

import (
"net"
"time"
Expand Down
3 changes: 3 additions & 0 deletions tssh/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ type sshArgs struct {
DynamicForward bindArgs `arg:"-D,--" placeholder:"[bind_addr:]port" help:"dynamic port forwarding ( socks5 proxy )"`
LocalForward forwardArgs `arg:"-L,--" placeholder:"[bind_addr:]port:host:hostport" help:"local port forwarding"`
RemoteForward forwardArgs `arg:"-R,--" placeholder:"[bind_addr:]port:host:hostport" help:"remote port forwarding"`
X11Untrusted bool `arg:"-X,--" help:"enables X11 forwarding"`
NoX11Forward bool `arg:"-x,--" help:"disables X11 forwarding"`
X11Trusted bool `arg:"-Y,--" help:"enables trusted X11 forwarding"`
Reconnect bool `arg:"--reconnect" help:"reconnect when background(-f) process exits"`
DragFile bool `arg:"--dragfile" help:"enable drag files and directories to upload"`
TraceLog bool `arg:"--tracelog" help:"enable trzsz detect trace logs for debugging"`
Expand Down
4 changes: 4 additions & 0 deletions tssh/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func TestSshArgs(t *testing.T) {
assertArgsEqual("-N", sshArgs{NoCommand: true})
assertArgsEqual("-gfN -T", sshArgs{Gateway: true, Background: true, NoCommand: true, DisableTTY: true})

assertArgsEqual("-X", sshArgs{X11Untrusted: true})
assertArgsEqual("-x", sshArgs{NoX11Forward: true})
assertArgsEqual("-Y", sshArgs{X11Trusted: true})

assertArgsEqual("-p1022", sshArgs{Port: 1022})
assertArgsEqual("-p 2049", sshArgs{Port: 2049})
assertArgsEqual("-luser", sshArgs{LoginName: "user"})
Expand Down
4 changes: 2 additions & 2 deletions tssh/cipher.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
package tssh

/*
MIT License
Expand All @@ -24,6 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package tssh

import (
"fmt"
"regexp"
Expand Down
165 changes: 165 additions & 0 deletions tssh/forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,168 @@ func sshForward(client *ssh.Client, args *sshArgs, param *sshParam) error {

return nil
}

type x11Request struct {
SingleConnection bool
AuthProtocol string
AuthCookie string
ScreenNumber uint32
}

func sshX11Forward(args *sshArgs, client *ssh.Client, session *ssh.Session) {
if args.NoX11Forward || !args.X11Untrusted && !args.X11Trusted && strings.ToLower(getOptionConfig(args, "ForwardX11")) != "yes" {
return
}

display := os.Getenv("DISPLAY")
if display == "" {
warning("X11 forwarding is not working since environment variable DISPLAY is not set")
return
}
hostname, displayNumber := resolveDisplayEnv(display)

trusted := false
if !args.X11Untrusted && (args.X11Trusted || strings.ToLower(getOptionConfig(args, "ForwardX11Trusted")) == "yes") {
trusted = true
}

timeout := 1200
if !trusted {
forwardX11Timeout := getOptionConfig(args, "ForwardX11Timeout")
if forwardX11Timeout != "" && strings.ToLower(forwardX11Timeout) != "none" {
seconds, err := convertSshTime(forwardX11Timeout)
if err != nil {
warning("invalid ForwardX11Timeout '%s': %v", forwardX11Timeout, err)
} else {
timeout = seconds
}
}
}

cookie, proto, err := getXauthAndProto(display, trusted, timeout)
if err != nil {
warning("X11 forwarding get xauth failed: %v", err)
return
}

payload := x11Request{
SingleConnection: false,
AuthProtocol: proto,
AuthCookie: cookie,
ScreenNumber: 0,
}
ok, err := session.SendRequest("x11-req", true, ssh.Marshal(payload))
if err != nil {
warning("X11 forwarding request failed: %v", err)
return
}
if !ok {
warning("X11 forwarding request denied")
return
}

channels := client.HandleChannelOpen("x11")
if channels == nil {
warning("already have handler for x11")
return
}
go func() {
for ch := range channels {
channel, reqs, err := ch.Accept()
if err != nil {
continue
}
go ssh.DiscardRequests(reqs)
go func() {
serveX11(display, hostname, displayNumber, channel)
channel.Close()
}()
}
}()
}

func resolveDisplayEnv(display string) (string, int) {
colon := strings.LastIndex(display, ":")
if colon < 0 {
return "", 0
}
hostname := display[:colon]
display = display[colon+1:]
dot := strings.Index(display, ".")
if dot < 0 {
dot = len(display)
}
displayNumber, err := strconv.Atoi(display[:dot])
if err != nil {
return "", 0
}
return hostname, displayNumber
}

func convertSshTime(time string) (int, error) {
total := 0
seconds := 0
for _, ch := range time {
switch {
case ch >= '0' && ch <= '9':
seconds = seconds*10 + int(ch-'0')
case ch == 's' || ch == 'S':
total += seconds
seconds = 0
case ch == 'm' || ch == 'M':
total += seconds * 60
seconds = 0
case ch == 'h' || ch == 'H':
total += seconds * 60 * 60
seconds = 0
case ch == 'd' || ch == 'D':
total += seconds * 60 * 60 * 24
seconds = 0
case ch == 'w' || ch == 'W':
total += seconds * 60 * 60 * 24 * 7
seconds = 0
default:
return 0, fmt.Errorf("invalid char '%c'", ch)
}
}
return total + seconds, nil
}

func serveX11(display, hostname string, displayNumber int, channel ssh.Channel) {
var err error
var conn net.Conn
if hostname != "" && !strings.HasPrefix(hostname, "/") {
conn, err = net.DialTimeout("tcp", joinHostPort(hostname, strconv.Itoa(6000+displayNumber)), time.Second)
} else if strings.HasPrefix(display, "/") {
conn, err = net.DialTimeout("unix", display, time.Second)
} else {
conn, err = net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", displayNumber), time.Second)
}
if err != nil {
debug("X11 forwarding dial [%s] failed: %v", display, err)
return
}

forwardChannel(channel, conn)
}

func forwardChannel(channel ssh.Channel, conn net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
_, _ = io.Copy(conn, channel)
if unixConn, ok := conn.(*net.UnixConn); ok {
_ = unixConn.CloseWrite()
}
wg.Done()
}()
go func() {
_, _ = io.Copy(channel, conn)
_ = channel.CloseWrite()
wg.Done()
}()

wg.Wait()
conn.Close()
channel.Close()
}
20 changes: 20 additions & 0 deletions tssh/forward_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,23 @@ func TestParseForwardArg(t *testing.T) {
assertArgError("127.0.0.1:8000:[::1]]:9000", "invalid forward specification: 127.0.0.1:8000:[::1]]:9000")
assertArgError("127.0.0.1:8000:[:\t:1]:9000", "invalid forward specification: 127.0.0.1:8000:[:\t:1]:9000")
}

func TestConvertSshTime(t *testing.T) {
assert := assert.New(t)
assertTimeEqual := func(time string, expected int) {
t.Helper()
seconds, err := convertSshTime(time)
assert.Nil(err)
assert.Equal(expected, seconds)
}
assertTimeEqual("0", 0)
assertTimeEqual("0s", 0)
assertTimeEqual("0W", 0)
assertTimeEqual("1", 1)
assertTimeEqual("1S", 1)
assertTimeEqual("90m", 5400)
assertTimeEqual("1h30m", 5400)
assertTimeEqual("2d", 172800)
assertTimeEqual("1w", 604800)
assertTimeEqual("1W2d3h4m5", 788645)
}
5 changes: 4 additions & 1 deletion tssh/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,9 +1268,12 @@ func sshLogin(args *sshArgs) (ss *sshSession, err error) {
return
}

// ssh agent forward
if !control {
// ssh agent forward
sshAgentForward(args, param, ss.client, ss.session)

// x11 forward
sshX11Forward(args, ss.client, ss.session)
}

// not terminal or not tty
Expand Down
Loading

0 comments on commit 3066b3c

Please sign in to comment.