commit
cbfef0c8b7
@ -0,0 +1,205 @@ |
||||
// 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.
|
||||
|
||||
// +build !windows
|
||||
|
||||
// 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", 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) |
||||
} |
||||
|
||||
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}))) |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
function remove_test_files { |
||||
rm -f ./*test{,.exe} |
||||
} |
||||
|
||||
function fail { |
||||
printf '%s\n' "$1" >&2 |
||||
# If we fail, clean up after ourselves |
||||
remove_test_files |
||||
exit 1 |
||||
} |
||||
|
||||
function main { |
||||
test_dirs=() |
||||
while IFS= read -r -d '' file |
||||
do |
||||
dir=$(dirname "$file") |
||||
if [[ ! " ${test_dirs[*]} " =~ ${dir} ]]; then |
||||
test_dirs+=("$dir") |
||||
fi |
||||
done < <(find . -type f -iname '*_test.go' -print0) |
||||
|
||||
for goos in openbsd darwin windows |
||||
do |
||||
for dir in "${test_dirs[@]}"; do |
||||
echo "Testing GOOS=$goos in dir $dir" |
||||
GOOS="$goos" go test -c "./$dir" || fail "Test failed using $goos and $dir" |
||||
done |
||||
done |
||||
|
||||
# If all goes well, we should still clean up the test files |
||||
echo "Test complete" |
||||
remove_test_files |
||||
} |
||||
|
||||
main "$@" |
||||
|
||||
@ -0,0 +1,176 @@ |
||||
// 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.
|
||||
|
||||
package wgengine |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
"net" |
||||
"os/exec" |
||||
|
||||
"github.com/tailscale/wireguard-go/device" |
||||
"github.com/tailscale/wireguard-go/tun" |
||||
"github.com/tailscale/wireguard-go/wgcfg" |
||||
"tailscale.com/logger" |
||||
) |
||||
|
||||
// For now this router only supports the userspace WireGuard implementations.
|
||||
//
|
||||
// There is an experimental kernel version in the works:
|
||||
// https://git.zx2c4.com/wireguard-openbsd.
|
||||
//
|
||||
// TODO(mbaillie): netlink-style monitoring might be possible through
|
||||
// `ifstated(8)`/`devd(8)`, or become possible with the OpenBSD kernel
|
||||
// implementation. This merits further investigation.
|
||||
|
||||
type openbsdRouter struct { |
||||
logf logger.Logf |
||||
tunname string |
||||
local wgcfg.CIDR |
||||
routes map[wgcfg.CIDR]struct{} |
||||
} |
||||
|
||||
func NewUserspaceRouter(logf logger.Logf, tunname string, _ *device.Device, tuntap tun.Device, _ func()) Router { |
||||
r := openbsdRouter{ |
||||
logf: logf, |
||||
tunname: tunname, |
||||
} |
||||
return &r |
||||
} |
||||
|
||||
// TODO(mbaillie): extract as identical to linux version
|
||||
func cmd(args ...string) *exec.Cmd { |
||||
if len(args) == 0 { |
||||
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]\n", args) |
||||
} |
||||
return exec.Command(args[0], args[1:]...) |
||||
} |
||||
|
||||
func (r *openbsdRouter) Up() error { |
||||
ifup := []string{"ifconfig", r.tunname, "up"} |
||||
if out, err := cmd(ifup...).CombinedOutput(); err != nil { |
||||
r.logf("running ifconfig failed: %v\n%s", err, out) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (r *openbsdRouter) SetRoutes(rs RouteSettings) error { |
||||
var errq error |
||||
|
||||
if rs.LocalAddr != r.local { |
||||
if r.local != (wgcfg.CIDR{}) { |
||||
addrdel := []string{"ifconfig", r.tunname, |
||||
"inet", r.local.String(), "-alias"} |
||||
out, err := cmd(addrdel...).CombinedOutput() |
||||
if err != nil { |
||||
r.logf("addr del failed: %v: %v\n%s", addrdel, err, out) |
||||
if errq == nil { |
||||
errq = err |
||||
} |
||||
} |
||||
|
||||
routedel := []string{"route", "-q", "-n", |
||||
"del", "-inet", r.local.String(), |
||||
"-iface", r.local.IP.String()} |
||||
if out, err := cmd(routedel...).CombinedOutput(); err != nil { |
||||
r.logf("route del failed: %v: %v\n%s", routedel, err, out) |
||||
if errq == nil { |
||||
errq = err |
||||
} |
||||
} |
||||
} |
||||
|
||||
addradd := []string{"ifconfig", r.tunname, |
||||
"inet", rs.LocalAddr.String(), "alias"} |
||||
out, err := cmd(addradd...).CombinedOutput() |
||||
if err != nil { |
||||
r.logf("addr add failed: %v: %v\n%s", addradd, err, out) |
||||
if errq == nil { |
||||
errq = err |
||||
} |
||||
} |
||||
|
||||
routeadd := []string{"route", "-q", "-n", |
||||
"add", "-inet", rs.LocalAddr.String(), |
||||
"-iface", rs.LocalAddr.IP.String()} |
||||
if out, err := cmd(routeadd...).CombinedOutput(); err != nil { |
||||
r.logf("route add failed: %v: %v\n%s", routeadd, err, out) |
||||
if errq == nil { |
||||
errq = err |
||||
} |
||||
} |
||||
} |
||||
|
||||
newRoutes := make(map[wgcfg.CIDR]struct{}) |
||||
for _, peer := range rs.Cfg.Peers { |
||||
for _, route := range peer.AllowedIPs { |
||||
newRoutes[route] = struct{}{} |
||||
} |
||||
} |
||||
for route := range r.routes { |
||||
if _, keep := newRoutes[route]; !keep { |
||||
net := route.IPNet() |
||||
nip := net.IP.Mask(net.Mask) |
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Mask) |
||||
routedel := []string{"route", "-q", "-n", |
||||
"del", "-inet", nstr, |
||||
"-iface", rs.LocalAddr.IP.String()} |
||||
out, err := cmd(routedel...).CombinedOutput() |
||||
if err != nil { |
||||
r.logf("route del failed: %v: %v\n%s", routedel, err, out) |
||||
if errq == nil { |
||||
errq = err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
for route := range newRoutes { |
||||
if _, exists := r.routes[route]; !exists { |
||||
net := route.IPNet() |
||||
nip := net.IP.Mask(net.Mask) |
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Mask) |
||||
routeadd := []string{"route", "-q", "-n", |
||||
"add", "-inet", nstr, |
||||
"-iface", rs.LocalAddr.IP.String()} |
||||
out, err := cmd(routeadd...).CombinedOutput() |
||||
if err != nil { |
||||
r.logf("addr add failed: %v: %v\n%s", routeadd, err, out) |
||||
if errq == nil { |
||||
errq = err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
r.local = rs.LocalAddr |
||||
r.routes = newRoutes |
||||
|
||||
if err := r.replaceResolvConf(rs.DNS, rs.DNSDomains); err != nil { |
||||
errq = fmt.Errorf("replacing resolv.conf failed: %v", err) |
||||
} |
||||
|
||||
return errq |
||||
} |
||||
|
||||
func (r *openbsdRouter) Close() error { |
||||
out, err := cmd("ifconfig", r.tunname, "down").CombinedOutput() |
||||
if err != nil { |
||||
r.logf("running ifconfig failed: %v\n%s", err, out) |
||||
} |
||||
|
||||
if err := r.restoreResolvConf(); err != nil { |
||||
r.logf("failed to restore system resolv.conf: %v", err) |
||||
} |
||||
|
||||
// TODO(mbaillie): wipe routes
|
||||
|
||||
return nil |
||||
} |
||||
|
||||
// TODO(mbaillie): these are no-ops for now. They could re-use the Linux funcs
|
||||
// (sans systemd parts), but I note Linux DNS is disabled(?) so leaving for now.
|
||||
func (r *openbsdRouter) replaceResolvConf(_ []net.IP, _ []string) error { return nil } |
||||
func (r *openbsdRouter) restoreResolvConf() error { return nil } |
||||
Loading…
Reference in new issue