parent
7e5e32775a
commit
d404f1caed
@ -0,0 +1,204 @@ |
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The tsshd binary is an SSH server that accepts connections
|
||||
// from anybody on the same Tailscale network.
|
||||
//
|
||||
// It does not use passwords or SSH public key.
|
||||
//
|
||||
// Any user name is accepted; users are logged in as whoever is
|
||||
// running this daemon.
|
||||
//
|
||||
// Warning: use at your own risk. This code has had very few eyeballs
|
||||
// on it.
|
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"log" |
||||
"net" |
||||
"os" |
||||
"os/exec" |
||||
"strings" |
||||
"syscall" |
||||
"time" |
||||
"unsafe" |
||||
|
||||
"github.com/gliderlabs/ssh" |
||||
"github.com/kr/pty" |
||||
gossh "golang.org/x/crypto/ssh" |
||||
) |
||||
|
||||
var ( |
||||
port = flag.Int("port", 2200, "port to listen on") |
||||
hostKey = flag.String("hostkey", "", "SSH host key") |
||||
) |
||||
|
||||
func main() { |
||||
flag.Parse() |
||||
if *hostKey == "" { |
||||
log.Fatalf("missing required --hostkey") |
||||
} |
||||
hostKey, err := ioutil.ReadFile(*hostKey) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
signer, err := gossh.ParsePrivateKey(hostKey) |
||||
if err != nil { |
||||
log.Printf("failed to parse SSH host key: %v; running running SSH server", err) |
||||
return |
||||
} |
||||
|
||||
warned := false |
||||
for { |
||||
addr, iface, err := tailscaleInterface() |
||||
if err != nil { |
||||
log.Fatalf("listing interfaces: %v", err) |
||||
} |
||||
if addr == nil { |
||||
if !warned { |
||||
log.Printf("no tailscale interface found; polling until one is available") |
||||
warned = true |
||||
} |
||||
// TODO: use netlink or other OS-specific mechanism to efficiently
|
||||
// wait for change in interfaces. Polling every N seconds is good enough
|
||||
// for now.
|
||||
time.Sleep(5 * time.Second) |
||||
continue |
||||
} |
||||
warned = false |
||||
listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port)) |
||||
log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen) |
||||
s := &ssh.Server{ |
||||
Addr: listen, |
||||
Handler: handleSSH, |
||||
} |
||||
s.AddHostKey(signer) |
||||
|
||||
err = s.ListenAndServe() |
||||
log.Fatalf("tailscale sshd failed: %v", err) |
||||
} |
||||
|
||||
} |
||||
|
||||
// tailscaleInterface returns an err on a fatal problem, and all zero values
|
||||
// if no suitable inteface is found.
|
||||
func tailscaleInterface() (net.IP, *net.Interface, error) { |
||||
ifs, err := net.Interfaces() |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
for _, iface := range ifs { |
||||
if !maybeTailscaleInterfaceName(iface.Name) { |
||||
continue |
||||
} |
||||
addrs, err := iface.Addrs() |
||||
if err != nil { |
||||
continue |
||||
} |
||||
for _, a := range addrs { |
||||
if ipnet, ok := a.(*net.IPNet); ok && isTailscaleIP(ipnet.IP) { |
||||
return ipnet.IP, &iface, nil |
||||
} |
||||
} |
||||
} |
||||
return nil, nil, nil |
||||
} |
||||
|
||||
// maybeTailscaleInterfaceName reports whether s is an interface
|
||||
// name that might be used by Tailscale.
|
||||
func maybeTailscaleInterfaceName(s string) bool { |
||||
return strings.HasPrefix(s, "wg") || |
||||
strings.HasPrefix(s, "ts") || |
||||
strings.HasPrefix(s, "tailscale") |
||||
} |
||||
|
||||
func isTailscaleIP(ip net.IP) bool { |
||||
return cgNAT.Contains(ip) |
||||
} |
||||
|
||||
var cgNAT = func() *net.IPNet { |
||||
_, ipNet, err := net.ParseCIDR("100.64.0.0/10") |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return ipNet |
||||
}() |
||||
|
||||
func handleSSH(s ssh.Session) { |
||||
user := s.User() |
||||
addr := s.RemoteAddr() |
||||
ta, ok := addr.(*net.TCPAddr) |
||||
if !ok { |
||||
log.Printf("tsshd: rejecting non-TCP addr %T %v", addr, addr) |
||||
s.Exit(1) |
||||
return |
||||
} |
||||
if !isTailscaleIP(ta.IP) { |
||||
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP) |
||||
s.Exit(1) |
||||
return |
||||
} |
||||
|
||||
log.Printf("new session for %q from %v", user, ta) |
||||
defer log.Printf("closing session for %q from %v", user, ta) |
||||
ptyReq, winCh, isPty := s.Pty() |
||||
if !isPty { |
||||
fmt.Fprintf(s, "TODO scp etc") |
||||
s.Exit(1) |
||||
return |
||||
} |
||||
|
||||
userWantsShell := len(s.Command()) == 0 |
||||
|
||||
if userWantsShell { |
||||
shell, err := shellOfUser(s.User()) |
||||
if err != nil { |
||||
fmt.Fprintf(s, "failed to find shell: %v\n", err) |
||||
s.Exit(1) |
||||
return |
||||
} |
||||
cmd := exec.Command(shell) |
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) |
||||
f, err := pty.Start(cmd) |
||||
if err != nil { |
||||
log.Printf("running shell: %v", err) |
||||
s.Exit(1) |
||||
return |
||||
} |
||||
defer f.Close() |
||||
go func() { |
||||
for win := range winCh { |
||||
setWinsize(f, win.Width, win.Height) |
||||
} |
||||
}() |
||||
go func() { |
||||
io.Copy(f, s) // stdin
|
||||
}() |
||||
io.Copy(s, f) // stdout
|
||||
cmd.Process.Kill() |
||||
if err := cmd.Wait(); err != nil { |
||||
s.Exit(1) |
||||
} |
||||
s.Exit(0) |
||||
return |
||||
} |
||||
|
||||
fmt.Fprintf(s, "TODO: args\n") |
||||
s.Exit(1) |
||||
return |
||||
} |
||||
|
||||
func shellOfUser(user string) (string, error) { |
||||
// TODO
|
||||
return "/bin/bash", nil |
||||
} |
||||
|
||||
func setWinsize(f *os.File, w, h int) { |
||||
syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), |
||||
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) |
||||
} |
||||
Loading…
Reference in new issue