508 lines
13 KiB
Go
508 lines
13 KiB
Go
// Copyright 2011 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Listen requests the remote peer open a listening socket on
|
|
// addr. Incoming connections will be available by calling Accept on
|
|
// the returned net.Listener. The listener must be serviced, or the
|
|
// SSH connection may hang.
|
|
// N must be "tcp", "tcp4", "tcp6", or "unix".
|
|
func (c *Client) Listen(n, addr string) (net.Listener, error) {
|
|
switch n {
|
|
case "tcp", "tcp4", "tcp6":
|
|
laddr, err := net.ResolveTCPAddr(n, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.ListenTCP(laddr)
|
|
case "unix":
|
|
return c.ListenUnix(addr)
|
|
default:
|
|
return nil, fmt.Errorf("ssh: unsupported protocol: %s", n)
|
|
}
|
|
}
|
|
|
|
// Automatic port allocation is broken with OpenSSH before 6.0. See
|
|
// also https://bugzilla.mindrot.org/show_bug.cgi?id=2017. In
|
|
// particular, OpenSSH 5.9 sends a channelOpenMsg with port number 0,
|
|
// rather than the actual port number. This means you can never open
|
|
// two different listeners with auto allocated ports. We work around
|
|
// this by trying explicit ports until we succeed.
|
|
|
|
const openSSHPrefix = "OpenSSH_"
|
|
|
|
var portRandomizer = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
// isBrokenOpenSSHVersion returns true if the given version string
|
|
// specifies a version of OpenSSH that is known to have a bug in port
|
|
// forwarding.
|
|
func isBrokenOpenSSHVersion(versionStr string) bool {
|
|
i := strings.Index(versionStr, openSSHPrefix)
|
|
if i < 0 {
|
|
return false
|
|
}
|
|
i += len(openSSHPrefix)
|
|
j := i
|
|
for ; j < len(versionStr); j++ {
|
|
if versionStr[j] < '0' || versionStr[j] > '9' {
|
|
break
|
|
}
|
|
}
|
|
version, _ := strconv.Atoi(versionStr[i:j])
|
|
return version < 6
|
|
}
|
|
|
|
// autoPortListenWorkaround simulates automatic port allocation by
|
|
// trying random ports repeatedly.
|
|
func (c *Client) autoPortListenWorkaround(laddr *net.TCPAddr) (net.Listener, error) {
|
|
var sshListener net.Listener
|
|
var err error
|
|
const tries = 10
|
|
for i := 0; i < tries; i++ {
|
|
addr := *laddr
|
|
addr.Port = 1024 + portRandomizer.Intn(60000)
|
|
sshListener, err = c.ListenTCP(&addr)
|
|
if err == nil {
|
|
laddr.Port = addr.Port
|
|
return sshListener, err
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("ssh: listen on random port failed after %d tries: %v", tries, err)
|
|
}
|
|
|
|
// RFC 4254 7.1
|
|
type channelForwardMsg struct {
|
|
addr string
|
|
rport uint32
|
|
}
|
|
|
|
// handleForwards starts goroutines handling forwarded connections.
|
|
// It's called on first use by (*Client).ListenTCP to not launch
|
|
// goroutines until needed.
|
|
func (c *Client) handleForwards() {
|
|
go c.forwards.handleChannels(c.HandleChannelOpen("forwarded-tcpip"))
|
|
go c.forwards.handleChannels(c.HandleChannelOpen("forwarded-streamlocal@openssh.com"))
|
|
}
|
|
|
|
// ListenTCP requests the remote peer open a listening socket
|
|
// on laddr. Incoming connections will be available by calling
|
|
// Accept on the returned net.Listener.
|
|
func (c *Client) ListenTCP(laddr *net.TCPAddr, fakeHost ...string) (net.Listener, error) {
|
|
c.handleForwardsOnce.Do(c.handleForwards)
|
|
if laddr.Port == 0 && isBrokenOpenSSHVersion(string(c.ServerVersion())) {
|
|
return c.autoPortListenWorkaround(laddr)
|
|
}
|
|
|
|
host := laddr.IP.String()
|
|
if len(fakeHost) > 0 {
|
|
host = fakeHost[0]
|
|
}
|
|
m := channelForwardMsg{
|
|
host,
|
|
uint32(laddr.Port),
|
|
}
|
|
// send message
|
|
ok, resp, err := c.SendRequest("tcpip-forward", true, Marshal(&m))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, errors.New("ssh: tcpip-forward request denied by peer")
|
|
}
|
|
|
|
// If the original port was 0, then the remote side will
|
|
// supply a real port number in the response.
|
|
if laddr.Port == 0 {
|
|
var p struct {
|
|
Port uint32
|
|
}
|
|
if err := Unmarshal(resp, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
laddr.Port = int(p.Port)
|
|
}
|
|
|
|
// Register this forward, using the port number we obtained.
|
|
var ch chan forward
|
|
if len(fakeHost) > 0 {
|
|
ch = c.forwards.add(laddr, fakeHost[0])
|
|
} else {
|
|
ch = c.forwards.add(laddr)
|
|
}
|
|
|
|
return &tcpListener{laddr, c, ch}, nil
|
|
}
|
|
|
|
// forwardList stores a mapping between remote
|
|
// forward requests and the tcpListeners.
|
|
type forwardList struct {
|
|
sync.Mutex
|
|
fdm sync.RWMutex
|
|
fakeDomainsMap map[string]string
|
|
entries []forwardEntry
|
|
}
|
|
|
|
// forwardEntry represents an established mapping of a laddr on a
|
|
// remote ssh server to a channel connected to a tcpListener.
|
|
type forwardEntry struct {
|
|
laddr net.Addr
|
|
c chan forward
|
|
}
|
|
|
|
// forward represents an incoming forwarded tcpip connection. The
|
|
// arguments to add/remove/lookup should be address as specified in
|
|
// the original forward-request.
|
|
type forward struct {
|
|
newCh NewChannel // the ssh client channel underlying this forward
|
|
raddr net.Addr // the raddr of the incoming connection
|
|
}
|
|
|
|
func (l *forwardList) add(addr net.Addr, fakeHost ...string) chan forward {
|
|
l.Lock()
|
|
defer l.Unlock()
|
|
f := forwardEntry{
|
|
laddr: addr,
|
|
c: make(chan forward, 1),
|
|
}
|
|
if len(fakeHost) > 0 {
|
|
if l.fakeDomainsMap == nil {
|
|
l.fakeDomainsMap = make(map[string]string)
|
|
}
|
|
defer l.fdm.Unlock()
|
|
l.fdm.Lock()
|
|
l.fakeDomainsMap[fakeHost[0]] = l.ipFromAddr(addr)
|
|
}
|
|
l.entries = append(l.entries, f)
|
|
return f.c
|
|
}
|
|
|
|
func (l *forwardList) ipFromAddr(addr net.Addr) string {
|
|
host, _, _ := net.SplitHostPort(addr.String())
|
|
return host
|
|
}
|
|
|
|
// See RFC 4254, section 7.2
|
|
type forwardedTCPPayload struct {
|
|
Addr string
|
|
Port uint32
|
|
OriginAddr string
|
|
OriginPort uint32
|
|
}
|
|
|
|
// parseTCPAddr parses the originating address from the remote into a *net.TCPAddr.
|
|
func parseTCPAddr(addr string, port uint32) (*net.TCPAddr, error) {
|
|
if port == 0 || port > 65535 {
|
|
return nil, fmt.Errorf("ssh: port number out of range: %d", port)
|
|
}
|
|
ip := net.ParseIP(string(addr))
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("ssh: cannot parse IP address %q", addr)
|
|
}
|
|
return &net.TCPAddr{IP: ip, Port: int(port)}, nil
|
|
}
|
|
|
|
func (l *forwardList) handleChannels(in <-chan NewChannel) {
|
|
for ch := range in {
|
|
var (
|
|
laddr net.Addr
|
|
raddr net.Addr
|
|
err error
|
|
)
|
|
switch channelType := ch.ChannelType(); channelType {
|
|
case "forwarded-tcpip":
|
|
var payload forwardedTCPPayload
|
|
if err = Unmarshal(ch.ExtraData(), &payload); err != nil {
|
|
ch.Reject(ConnectionFailed, "could not parse forwarded-tcpip payload: "+err.Error())
|
|
continue
|
|
}
|
|
|
|
l.fdm.RLock()
|
|
if addr, ok := l.fakeDomainsMap[payload.Addr]; ok {
|
|
payload.Addr = addr
|
|
}
|
|
if addr, ok := l.fakeDomainsMap[payload.OriginAddr]; ok {
|
|
payload.OriginAddr = addr
|
|
}
|
|
l.fdm.RUnlock()
|
|
|
|
// RFC 4254 section 7.2 specifies that incoming
|
|
// addresses should list the address, in string
|
|
// format. It is implied that this should be an IP
|
|
// address, as it would be impossible to connect to it
|
|
// otherwise.
|
|
laddr, err = parseTCPAddr(payload.Addr, payload.Port)
|
|
if err != nil {
|
|
ch.Reject(ConnectionFailed, err.Error())
|
|
continue
|
|
}
|
|
raddr, err = parseTCPAddr(payload.OriginAddr, payload.OriginPort)
|
|
if err != nil {
|
|
ch.Reject(ConnectionFailed, err.Error())
|
|
continue
|
|
}
|
|
|
|
case "forwarded-streamlocal@openssh.com":
|
|
var payload forwardedStreamLocalPayload
|
|
if err = Unmarshal(ch.ExtraData(), &payload); err != nil {
|
|
ch.Reject(ConnectionFailed, "could not parse forwarded-streamlocal@openssh.com payload: "+err.Error())
|
|
continue
|
|
}
|
|
laddr = &net.UnixAddr{
|
|
Name: payload.SocketPath,
|
|
Net: "unix",
|
|
}
|
|
raddr = &net.UnixAddr{
|
|
Name: "@",
|
|
Net: "unix",
|
|
}
|
|
default:
|
|
panic(fmt.Errorf("ssh: unknown channel type %s", channelType))
|
|
}
|
|
if ok := l.forward(laddr, raddr, ch); !ok {
|
|
// Section 7.2, implementations MUST reject spurious incoming
|
|
// connections.
|
|
ch.Reject(Prohibited, "no forward for address")
|
|
continue
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// remove removes the forward entry, and the channel feeding its
|
|
// listener.
|
|
func (l *forwardList) remove(addr net.Addr) {
|
|
l.Lock()
|
|
defer l.Unlock()
|
|
for i, f := range l.entries {
|
|
if addr.Network() == f.laddr.Network() && addr.String() == f.laddr.String() {
|
|
l.entries = append(l.entries[:i], l.entries[i+1:]...)
|
|
close(f.c)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// closeAll closes and clears all forwards.
|
|
func (l *forwardList) closeAll() {
|
|
l.Lock()
|
|
defer l.Unlock()
|
|
for _, f := range l.entries {
|
|
close(f.c)
|
|
}
|
|
l.entries = nil
|
|
}
|
|
|
|
func (l *forwardList) forward(laddr, raddr net.Addr, ch NewChannel) bool {
|
|
l.Lock()
|
|
defer l.Unlock()
|
|
for _, f := range l.entries {
|
|
if laddr.Network() == f.laddr.Network() && laddr.String() == f.laddr.String() {
|
|
f.c <- forward{newCh: ch, raddr: raddr}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type tcpListener struct {
|
|
laddr *net.TCPAddr
|
|
|
|
conn *Client
|
|
in <-chan forward
|
|
}
|
|
|
|
// Accept waits for and returns the next connection to the listener.
|
|
func (l *tcpListener) Accept() (net.Conn, error) {
|
|
s, ok := <-l.in
|
|
if !ok {
|
|
return nil, io.EOF
|
|
}
|
|
ch, incoming, err := s.newCh.Accept()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go DiscardRequests(incoming)
|
|
|
|
return &chanConn{
|
|
Channel: ch,
|
|
laddr: l.laddr,
|
|
raddr: s.raddr,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the listener.
|
|
func (l *tcpListener) Close() error {
|
|
m := channelForwardMsg{
|
|
l.laddr.IP.String(),
|
|
uint32(l.laddr.Port),
|
|
}
|
|
|
|
// this also closes the listener.
|
|
l.conn.forwards.remove(l.laddr)
|
|
ok, _, err := l.conn.SendRequest("cancel-tcpip-forward", true, Marshal(&m))
|
|
if err == nil && !ok {
|
|
err = errors.New("ssh: cancel-tcpip-forward failed")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Addr returns the listener's network address.
|
|
func (l *tcpListener) Addr() net.Addr {
|
|
return l.laddr
|
|
}
|
|
|
|
// Dial initiates a connection to the addr from the remote host.
|
|
// The resulting connection has a zero LocalAddr() and RemoteAddr().
|
|
func (c *Client) Dial(n, addr string) (net.Conn, error) {
|
|
var ch Channel
|
|
switch n {
|
|
case "tcp", "tcp4", "tcp6":
|
|
// Parse the address into host and numeric port.
|
|
host, portString, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port, err := strconv.ParseUint(portString, 10, 16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ch, err = c.dial(net.IPv4zero.String(), 0, host, int(port))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Use a zero address for local and remote address.
|
|
zeroAddr := &net.TCPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: 0,
|
|
}
|
|
return &chanConn{
|
|
Channel: ch,
|
|
laddr: zeroAddr,
|
|
raddr: zeroAddr,
|
|
}, nil
|
|
case "unix":
|
|
var err error
|
|
ch, err = c.dialStreamLocal(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &chanConn{
|
|
Channel: ch,
|
|
laddr: &net.UnixAddr{
|
|
Name: "@",
|
|
Net: "unix",
|
|
},
|
|
raddr: &net.UnixAddr{
|
|
Name: addr,
|
|
Net: "unix",
|
|
},
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("ssh: unsupported protocol: %s", n)
|
|
}
|
|
}
|
|
|
|
// DialTCP connects to the remote address raddr on the network net,
|
|
// which must be "tcp", "tcp4", or "tcp6". If laddr is not nil, it is used
|
|
// as the local address for the connection.
|
|
func (c *Client) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error) {
|
|
if laddr == nil {
|
|
laddr = &net.TCPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: 0,
|
|
}
|
|
}
|
|
ch, err := c.dial(laddr.IP.String(), laddr.Port, raddr.IP.String(), raddr.Port)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &chanConn{
|
|
Channel: ch,
|
|
laddr: laddr,
|
|
raddr: raddr,
|
|
}, nil
|
|
}
|
|
|
|
// RFC 4254 7.2
|
|
type channelOpenDirectMsg struct {
|
|
raddr string
|
|
rport uint32
|
|
laddr string
|
|
lport uint32
|
|
}
|
|
|
|
func (c *Client) dial(laddr string, lport int, raddr string, rport int) (Channel, error) {
|
|
msg := channelOpenDirectMsg{
|
|
raddr: raddr,
|
|
rport: uint32(rport),
|
|
laddr: laddr,
|
|
lport: uint32(lport),
|
|
}
|
|
ch, in, err := c.OpenChannel("direct-tcpip", Marshal(&msg))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go DiscardRequests(in)
|
|
return ch, err
|
|
}
|
|
|
|
type tcpChan struct {
|
|
Channel // the backing channel
|
|
}
|
|
|
|
// chanConn fulfills the net.Conn interface without
|
|
// the tcpChan having to hold laddr or raddr directly.
|
|
type chanConn struct {
|
|
Channel
|
|
laddr, raddr net.Addr
|
|
}
|
|
|
|
// LocalAddr returns the local network address.
|
|
func (t *chanConn) LocalAddr() net.Addr {
|
|
return t.laddr
|
|
}
|
|
|
|
// RemoteAddr returns the remote network address.
|
|
func (t *chanConn) RemoteAddr() net.Addr {
|
|
return t.raddr
|
|
}
|
|
|
|
// SetDeadline sets the read and write deadlines associated
|
|
// with the connection.
|
|
func (t *chanConn) SetDeadline(deadline time.Time) error {
|
|
if err := t.SetReadDeadline(deadline); err != nil {
|
|
return err
|
|
}
|
|
return t.SetWriteDeadline(deadline)
|
|
}
|
|
|
|
// SetReadDeadline sets the read deadline.
|
|
// A zero value for t means Read will not time out.
|
|
// After the deadline, the error from Read will implement net.Error
|
|
// with Timeout() == true.
|
|
func (t *chanConn) SetReadDeadline(deadline time.Time) error {
|
|
// for compatibility with previous version,
|
|
// the error message contains "tcpChan"
|
|
return errors.New("ssh: tcpChan: deadline not supported")
|
|
}
|
|
|
|
// SetWriteDeadline exists to satisfy the net.Conn interface
|
|
// but is not implemented by this type. It always returns an error.
|
|
func (t *chanConn) SetWriteDeadline(deadline time.Time) error {
|
|
return errors.New("ssh: tcpChan: deadline not supported")
|
|
}
|