Merge branch 'master' of /Users/jsimsa/Code/vanadium/release/go/src/v.io/apps
diff --git a/rps/common/common.go b/rps/common/common.go
new file mode 100644
index 0000000..4ab7bb4
--- /dev/null
+++ b/rps/common/common.go
@@ -0,0 +1,111 @@
+// Package common factors out common utility functions that both the
+// rock paper scissors clients and servers invoke.
+package common
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"math/rand"
+	"os"
+	"time"
+
+	"v.io/apps/rps"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/x/lib/vlog"
+)
+
+// CreateName creates a name using the username and hostname.
+func CreateName() string {
+	hostname, err := os.Hostname()
+	if err != nil {
+		vlog.Fatalf("os.Hostname failed: %v", err)
+	}
+	return os.Getenv("USER") + "@" + hostname
+}
+
+// FindJudge returns a random rock-paper-scissors judge from the mount table.
+func FindJudge(ctx *context.T) (string, error) {
+	judges, err := findAll(ctx, "judge")
+	if err != nil {
+		return "", err
+	}
+	if len(judges) > 0 {
+		return judges[rand.Intn(len(judges))], nil
+	}
+	return "", errors.New("no judges")
+}
+
+// FindPlayer returns a random rock-paper-scissors player from the mount table.
+func FindPlayer(ctx *context.T) (string, error) {
+	players, err := findAll(ctx, "player")
+	if err != nil {
+		return "", err
+	}
+	if len(players) > 0 {
+		return players[rand.Intn(len(players))], nil
+	}
+	return "", errors.New("no players")
+}
+
+// FindScoreKeepers returns all the rock-paper-scissors score keepers from the
+// mount table.
+func FindScoreKeepers(ctx *context.T) ([]string, error) {
+	sKeepers, err := findAll(ctx, "scorekeeper")
+	if err != nil {
+		return nil, err
+	}
+	return sKeepers, nil
+}
+
+func findAll(ctx *context.T, t string) ([]string, error) {
+	start := time.Now()
+	ns := v23.GetNamespace(ctx)
+	c, err := ns.Glob(ctx, "rps/"+t+"/*")
+	if err != nil {
+		vlog.Infof("mt.Glob failed: %v", err)
+		return nil, err
+	}
+	var servers []string
+	for e := range c {
+		switch v := e.(type) {
+		case *naming.GlobError:
+			vlog.VI(1).Infof("findAll(%q) error for %q: %v", t, v.Name, v.Error)
+		case *naming.MountEntry:
+			servers = append(servers, v.Name)
+		}
+	}
+	vlog.VI(1).Infof("findAll(%q) elapsed: %s", t, time.Now().Sub(start))
+	return servers, nil
+}
+
+// FormatScoreCard returns a string representation of a score card.
+func FormatScoreCard(score rps.ScoreCard) string {
+	buf := bytes.NewBufferString("")
+	var gameType string
+	switch score.Opts.GameType {
+	case rps.Classic:
+		gameType = "Classic"
+	case rps.LizardSpock:
+		gameType = "LizardSpock"
+	default:
+		gameType = "Unknown"
+	}
+	fmt.Fprintf(buf, "Game Type: %s\n", gameType)
+	fmt.Fprintf(buf, "Number of rounds: %d\n", score.Opts.NumRounds)
+	fmt.Fprintf(buf, "Judge: %s\n", score.Judge)
+	fmt.Fprintf(buf, "Player 1: %s\n", score.Players[0])
+	fmt.Fprintf(buf, "Player 2: %s\n", score.Players[1])
+	for i, r := range score.Rounds {
+		roundOffset := time.Duration(r.StartTimeNS - score.StartTimeNS)
+		roundTime := time.Duration(r.EndTimeNS - r.StartTimeNS)
+		fmt.Fprintf(buf, "Round %2d: Player 1 played %-10q. Player 2 played %-10q. Winner: %d %-28s [%-10s/%-10s]\n",
+			i+1, r.Moves[0], r.Moves[1], r.Winner, r.Comment, roundOffset, roundTime)
+	}
+	fmt.Fprintf(buf, "Winner: %d\n", score.Winner)
+	fmt.Fprintf(buf, "Time: %s\n", time.Duration(score.EndTimeNS-score.StartTimeNS))
+	return buf.String()
+}
diff --git a/rps/rpsbot/impl.go b/rps/rpsbot/impl.go
new file mode 100644
index 0000000..1942094
--- /dev/null
+++ b/rps/rpsbot/impl.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+	"errors"
+
+	"v.io/apps/rps"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/vtrace"
+	"v.io/x/lib/vlog"
+)
+
+// RPS implements rps.RockPaperScissorsServerMethods
+type RPS struct {
+	player      *Player
+	judge       *Judge
+	scoreKeeper *ScoreKeeper
+	ctx         *context.T
+}
+
+func NewRPS(ctx *context.T) *RPS {
+	return &RPS{player: NewPlayer(), judge: NewJudge(), scoreKeeper: NewScoreKeeper(), ctx: ctx}
+}
+
+func (r *RPS) Judge() *Judge {
+	return r.judge
+}
+
+func (r *RPS) Player() *Player {
+	return r.player
+}
+
+func (r *RPS) ScoreKeeper() *ScoreKeeper {
+	return r.scoreKeeper
+}
+
+func (r *RPS) CreateGame(ctx ipc.ServerContext, opts rps.GameOptions) (rps.GameID, error) {
+	if vlog.V(1) {
+		b, _ := ctx.RemoteBlessings().ForContext(ctx)
+		vlog.Infof("CreateGame %+v from %v", opts, b)
+	}
+	names, _ := ctx.LocalBlessings().ForContext(ctx)
+	if len(names) == 0 {
+		return rps.GameID{}, errors.New("no names provided for context")
+	}
+	return r.judge.createGame(names[0], opts)
+}
+
+func (r *RPS) Play(ctx rps.JudgePlayContext, id rps.GameID) (rps.PlayResult, error) {
+	names, _ := ctx.RemoteBlessings().ForContext(ctx)
+	vlog.VI(1).Infof("Play %+v from %v", id, names)
+	if len(names) == 0 {
+		return rps.PlayResult{}, errors.New("no names provided for context")
+	}
+	return r.judge.play(ctx, names[0], id)
+}
+
+func (r *RPS) Challenge(ctx ipc.ServerContext, address string, id rps.GameID, opts rps.GameOptions) error {
+	b, _ := ctx.RemoteBlessings().ForContext(ctx)
+	vlog.VI(1).Infof("Challenge (%q, %+v, %+v) from %v", address, id, opts, b)
+	newctx, _ := vtrace.SetNewTrace(r.ctx)
+	return r.player.challenge(newctx, address, id, opts)
+}
+
+func (r *RPS) Record(ctx ipc.ServerContext, score rps.ScoreCard) error {
+	b, _ := ctx.RemoteBlessings().ForContext(ctx)
+	vlog.VI(1).Infof("Record (%+v) from %v", score, b)
+	return r.scoreKeeper.Record(ctx, score)
+}
diff --git a/rps/rpsbot/impl_test.go b/rps/rpsbot/impl_test.go
new file mode 100644
index 0000000..e80fe67
--- /dev/null
+++ b/rps/rpsbot/impl_test.go
@@ -0,0 +1,180 @@
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"io/ioutil"
+	"os"
+	"regexp"
+	"runtime"
+	"runtime/pprof"
+	"strconv"
+	"testing"
+
+	"v.io/apps/rps"
+
+	"v.io/core/veyron/lib/testutil"
+	mtlib "v.io/core/veyron/services/mounttable/lib"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/options"
+	"v.io/x/lib/vlog"
+)
+
+var spec = ipc.ListenSpec{Addrs: ipc.ListenAddrs{{"tcp", "127.0.0.1:0"}}}
+
+func startMountTable(t *testing.T, ctx *context.T) (string, func()) {
+	server, err := v23.NewServer(ctx, options.ServesMountTable(true))
+	if err != nil {
+		t.Fatalf("NewServer() failed: %v", err)
+	}
+	dispatcher, err := mtlib.NewMountTableDispatcher("")
+	if err != nil {
+		t.Fatalf("NewMountTableDispatcher() failed: %v", err)
+	}
+	endpoints, err := server.Listen(spec)
+	if err != nil {
+		t.Fatalf("Listen(%v) failed: %v", spec, err)
+	}
+	if err := server.ServeDispatcher("", dispatcher); err != nil {
+		t.Fatalf("ServeDispatcher(%v) failed: %v", dispatcher, err)
+	}
+	address := naming.JoinAddressName(endpoints[0].String(), "")
+	vlog.VI(1).Infof("Mount table running at endpoint: %s", address)
+	return address, func() {
+		if err := server.Stop(); err != nil {
+			t.Fatalf("Stop() failed: %v", err)
+		}
+	}
+}
+
+func startRockPaperScissors(t *testing.T, ctx *context.T, mtAddress string) (*RPS, func()) {
+	ns := v23.GetNamespace(ctx)
+	ns.SetRoots(mtAddress)
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Fatalf("NewServer failed: %v", err)
+	}
+	rpsService := NewRPS(ctx)
+
+	if _, err = server.Listen(spec); err != nil {
+		t.Fatalf("Listen failed: %v", err)
+	}
+	names := []string{"rps/judge/test", "rps/player/test", "rps/scorekeeper/test"}
+	if err := server.Serve(names[0], rps.RockPaperScissorsServer(rpsService), nil); err != nil {
+		t.Fatalf("Serve(%v) failed: %v", names[0], err)
+	}
+	for _, n := range names[1:] {
+		server.AddName(n)
+	}
+	return rpsService, func() {
+		if err := server.Stop(); err != nil {
+			t.Fatalf("Stop() failed: %v", err)
+		}
+	}
+}
+
+// TestRockPaperScissorsImpl runs one rock-paper-scissors game and verifies
+// that all the counters are consistent.
+func TestRockPaperScissorsImpl(t *testing.T) {
+	ctx, shutdown := testutil.InitForTest()
+	defer shutdown()
+
+	mtAddress, mtStop := startMountTable(t, ctx)
+	defer mtStop()
+	rpsService, rpsStop := startRockPaperScissors(t, ctx, mtAddress)
+	defer rpsStop()
+
+	const numGames = 100
+	for x := 0; x < numGames; x++ {
+		if err := rpsService.Player().InitiateGame(ctx); err != nil {
+			t.Errorf("Failed to initiate game: %v", err)
+		}
+	}
+	rpsService.Player().WaitUntilIdle()
+
+	// For each game, the player plays twice. So, we expect the player to
+	// show that it played 2×numGames, and won numGames.
+	played, won := rpsService.Player().Stats()
+	if want, got := int64(2*numGames), played; want != got {
+		t.Errorf("Unexpected number of played games. Got %d, want %d", got, want)
+	}
+	if want, got := int64(numGames), won; want != got {
+		t.Errorf("Unexpected number of won games. Got %d, want %d", got, want)
+	}
+
+	// The Judge ran every game.
+	if want, got := int64(numGames), rpsService.Judge().Stats(); want != got {
+		t.Errorf("Unexpected number of games run. Got %d, want %d", got, want)
+	}
+
+	// The Score Keeper received one score card per game.
+	if want, got := int64(numGames), rpsService.ScoreKeeper().Stats(); want != got {
+		t.Errorf("Unexpected number of score cards. Got %d, want %d", got, want)
+	}
+
+	// Check for leaked goroutines.
+	if n := runtime.NumGoroutine(); n > 100 {
+		t.Logf("Num goroutines: %d", n)
+		if leak := reportLeakedGoroutines(t, n/3); len(leak) != 0 {
+			t.Errorf("Potentially leaked goroutines:\n%s", leak)
+		}
+	}
+}
+
+// reportLeakedGoroutines reads the goroutine pprof profile and returns the
+// goroutines that have more than 'threshold' instances.
+func reportLeakedGoroutines(t *testing.T, threshold int) string {
+	f, err := ioutil.TempFile("", "test-profile-")
+	if err != nil {
+		t.Fatalf("Failed to create temp file: %v", err)
+	}
+	defer f.Close()
+	defer os.Remove(f.Name())
+	p := pprof.Lookup("goroutine")
+	if p == nil {
+		t.Fatalf("Failed to lookup the goroutine profile")
+	}
+	if err := p.WriteTo(f, 1); err != nil {
+		t.Fatalf("Failed to write profile: %v", err)
+	}
+
+	re, err := regexp.Compile(`^([0-9]+) @`)
+	if err != nil {
+		t.Fatalf("Failed to compile regexp")
+	}
+	f.Seek(0, 0)
+	r := bufio.NewReader(f)
+
+	var buf bytes.Buffer
+	var printing bool
+	for {
+		line, err := r.ReadBytes('\n')
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			t.Fatalf("Failed to read bytes: %v", err)
+		}
+		if len(line) <= 1 {
+			printing = false
+			continue
+		}
+		if !printing {
+			if m := re.FindSubmatch(line); len(m) == 2 {
+				count, _ := strconv.Atoi(string(m[1]))
+				if count > threshold {
+					printing = true
+				}
+			}
+		}
+		if printing {
+			buf.Write(line)
+		}
+	}
+	return buf.String()
+}
diff --git a/rps/rpsbot/judge.go b/rps/rpsbot/judge.go
new file mode 100644
index 0000000..c7240dd
--- /dev/null
+++ b/rps/rpsbot/judge.go
@@ -0,0 +1,356 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+	"sync"
+	"time"
+
+	"v.io/apps/rps"
+	"v.io/apps/rps/common"
+	"v.io/core/veyron/lib/stats"
+	"v.io/core/veyron/lib/stats/counter"
+	"v.io/v23/context"
+	"v.io/x/lib/vlog"
+)
+
+var (
+	errBadGameID      = errors.New("requested gameID doesn't exist")
+	errTooManyPlayers = errors.New("all players are already seated")
+)
+
+type Judge struct {
+	lock     sync.Mutex
+	games    map[rps.GameID]*gameInfo
+	gamesRun *counter.Counter
+}
+
+type sendStream interface {
+	Send(item rps.JudgeAction) error
+}
+
+type gameInfo struct {
+	id        rps.GameID
+	startTime time.Time
+	score     rps.ScoreCard
+	streams   []sendStream
+	playerIn  chan playerInput
+	playerOut []chan rps.JudgeAction
+	scoreChan chan scoreData
+}
+
+func (g *gameInfo) moveOptions() []string {
+	switch g.score.Opts.GameType {
+	case rps.LizardSpock:
+		return []string{"rock", "paper", "scissors", "lizard", "spock"}
+	default:
+		return []string{"rock", "paper", "scissors"}
+	}
+}
+
+func (g *gameInfo) validMove(m string) bool {
+	for _, x := range g.moveOptions() {
+		if x == m {
+			return true
+		}
+	}
+	return false
+}
+
+type playerInput struct {
+	player int
+	action rps.PlayerAction
+}
+
+type scoreData struct {
+	err   error
+	score rps.ScoreCard
+}
+
+func NewJudge() *Judge {
+	return &Judge{
+		games:    make(map[rps.GameID]*gameInfo),
+		gamesRun: stats.NewCounter("judge/games-run"),
+	}
+}
+
+func (j *Judge) Stats() int64 {
+	return j.gamesRun.Value()
+}
+
+// createGame handles a request to create a new game.
+func (j *Judge) createGame(ownName string, opts rps.GameOptions) (rps.GameID, error) {
+	vlog.VI(1).Infof("createGame called")
+	score := rps.ScoreCard{Opts: opts, Judge: ownName}
+	now := time.Now()
+	id := rps.GameID{ID: strconv.FormatInt(now.UnixNano(), 16)}
+
+	j.lock.Lock()
+	defer j.lock.Unlock()
+	for k, v := range j.games {
+		if now.Sub(v.startTime) > 1*time.Hour {
+			vlog.Infof("Removing stale game ID %v", k)
+			delete(j.games, k)
+		}
+	}
+	j.games[id] = &gameInfo{
+		id:        id,
+		startTime: now,
+		score:     score,
+		playerIn:  make(chan playerInput),
+		playerOut: []chan rps.JudgeAction{
+			make(chan rps.JudgeAction),
+			make(chan rps.JudgeAction),
+		},
+		scoreChan: make(chan scoreData),
+	}
+	return id, nil
+}
+
+// play interacts with a player for the duration of a game.
+func (j *Judge) play(ctx rps.JudgePlayContext, name string, id rps.GameID) (rps.PlayResult, error) {
+	vlog.VI(1).Infof("play from %q for %v", name, id)
+	nilResult := rps.PlayResult{}
+
+	pIn, pOut, s, err := j.gameChannels(id)
+	if err != nil {
+		return nilResult, err
+	}
+	playerNum, err := j.addPlayer(name, id, ctx)
+	if err != nil {
+		return nilResult, err
+	}
+	vlog.VI(1).Infof("This is player %d", playerNum)
+
+	// Send all user input to the player input channel.
+	done := make(chan struct{})
+	defer close(done)
+	go func() {
+		rStream := ctx.RecvStream()
+		for rStream.Advance() {
+			action := rStream.Value()
+			select {
+			case pIn <- playerInput{player: playerNum, action: action}:
+			case <-done:
+				return
+			}
+		}
+		select {
+		case pIn <- playerInput{player: playerNum, action: rps.PlayerActionQuit{}}:
+		case <-done:
+		}
+	}()
+	// Send all the output to the user.
+	go func() {
+		for packet := range pOut[playerNum-1] {
+			if err := ctx.SendStream().Send(packet); err != nil {
+				vlog.Infof("error sending to player stream: %v", err)
+			}
+		}
+	}()
+	defer close(pOut[playerNum-1])
+
+	pOut[playerNum-1] <- rps.JudgeActionPlayerNum{int32(playerNum)}
+
+	// When the second player connects, we start the game.
+	if playerNum == 2 {
+		go j.manageGame(ctx.Context(), id)
+	}
+	// Wait for the ScoreCard.
+	scoreData := <-s
+	if scoreData.err != nil {
+		return rps.PlayResult{}, scoreData.err
+	}
+	return rps.PlayResult{YouWon: scoreData.score.Winner == rps.WinnerTag(playerNum)}, nil
+}
+
+func (j *Judge) manageGame(ctx *context.T, id rps.GameID) {
+	j.gamesRun.Incr(1)
+	j.lock.Lock()
+	info, exists := j.games[id]
+	if !exists {
+		e := scoreData{err: errBadGameID}
+		info.scoreChan <- e
+		info.scoreChan <- e
+		return
+	}
+	delete(j.games, id)
+	j.lock.Unlock()
+
+	// Inform each player of their opponent's name.
+	for p := 0; p < 2; p++ {
+		opp := 1 - p
+		info.playerOut[p] <- rps.JudgeActionOpponentName{info.score.Players[opp]}
+	}
+
+	win1, win2 := 0, 0
+	goal := int(info.score.Opts.NumRounds)
+	// Play until one player has won 'goal' times.
+	for win1 < goal && win2 < goal {
+		round, err := j.playOneRound(info)
+		if err != nil {
+			err := scoreData{err: err}
+			info.scoreChan <- err
+			info.scoreChan <- err
+			return
+		}
+		if round.Winner == rps.Player1 {
+			win1++
+		} else if round.Winner == rps.Player2 {
+			win2++
+		}
+		info.score.Rounds = append(info.score.Rounds, round)
+	}
+	if win1 > win2 {
+		info.score.Winner = rps.Player1
+	} else {
+		info.score.Winner = rps.Player2
+	}
+
+	info.score.StartTimeNS = info.startTime.UnixNano()
+	info.score.EndTimeNS = time.Now().UnixNano()
+
+	// Send the score card to the players.
+	action := rps.JudgeActionScore{info.score}
+	for _, p := range info.playerOut {
+		p <- action
+	}
+
+	// Send the score card to the score keepers.
+	scoreCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+	keepers, err := common.FindScoreKeepers(scoreCtx)
+	if err != nil || len(keepers) == 0 {
+		vlog.Infof("No score keepers: %v", err)
+		return
+	}
+	var wg sync.WaitGroup
+	wg.Add(len(keepers))
+	for _, k := range keepers {
+		go j.sendScore(scoreCtx, k, info.score, &wg)
+	}
+	wg.Wait()
+
+	info.scoreChan <- scoreData{score: info.score}
+	info.scoreChan <- scoreData{score: info.score}
+}
+
+func (j *Judge) playOneRound(info *gameInfo) (rps.Round, error) {
+	round := rps.Round{StartTimeNS: time.Now().UnixNano()}
+	var action rps.JudgeAction
+	action = rps.JudgeActionMoveOptions{info.moveOptions()}
+	for _, p := range info.playerOut {
+		p <- action
+	}
+	for x := 0; x < 2; x++ {
+		in := <-info.playerIn
+		switch v := in.action.(type) {
+		case rps.PlayerActionQuit:
+			return round, fmt.Errorf("player %d quit the game", in.player)
+		case rps.PlayerActionMove:
+			move := v.Value
+			if !info.validMove(move) {
+				return round, fmt.Errorf("player %d made an invalid move: %s", in.player, move)
+			}
+			if len(round.Moves[in.player-1]) > 0 {
+				return round, fmt.Errorf("player %d played twice in the same round!", in.player)
+			}
+			round.Moves[in.player-1] = move
+		default:
+			vlog.Infof("unexpected message type: %T", in.action)
+		}
+	}
+	round.Winner, round.Comment = j.compareMoves(round.Moves[0], round.Moves[1])
+	vlog.VI(1).Infof("Player 1 played %q. Player 2 played %q. Winner: %d %s", round.Moves[0], round.Moves[1], round.Winner, round.Comment)
+
+	action = rps.JudgeActionRoundResult{round}
+	for _, p := range info.playerOut {
+		p <- action
+	}
+	round.EndTimeNS = time.Now().UnixNano()
+	return round, nil
+}
+
+func (j *Judge) addPlayer(name string, id rps.GameID, stream rps.JudgePlayServerStream) (int, error) {
+	j.lock.Lock()
+	defer j.lock.Unlock()
+
+	info, exists := j.games[id]
+	if !exists {
+		return 0, errBadGameID
+	}
+	if len(info.streams) == 2 {
+		return 0, errTooManyPlayers
+	}
+	info.score.Players = append(info.score.Players, name)
+	info.streams = append(info.streams, stream.SendStream())
+	return len(info.streams), nil
+}
+
+func (j *Judge) gameChannels(id rps.GameID) (chan playerInput, []chan rps.JudgeAction, chan scoreData, error) {
+	j.lock.Lock()
+	defer j.lock.Unlock()
+	info, exists := j.games[id]
+	if !exists {
+		return nil, nil, nil, errBadGameID
+	}
+	return info.playerIn, info.playerOut, info.scoreChan, nil
+}
+
+func (j *Judge) sendScore(ctx *context.T, address string, score rps.ScoreCard, wg *sync.WaitGroup) error {
+	defer wg.Done()
+	k := rps.RockPaperScissorsClient(address)
+	if err := k.Record(ctx, score); err != nil {
+		vlog.Infof("Record: %v", err)
+		return err
+	}
+	return nil
+}
+
+var moveComments = map[string]string{
+	"lizard-paper":    "lizard eats paper",
+	"lizard-rock":     "rock crushes lizard",
+	"lizard-scissors": "scissors decapitates lizard",
+	"lizard-spock":    "lizard poisons spock",
+	"paper-rock":      "paper covers rock",
+	"paper-scissors":  "scissors cuts paper",
+	"paper-spock":     "paper disproves spock",
+	"rock-scissors":   "rock crushes scissors",
+	"rock-spock":      "spock vaporizes rock",
+	"scissors-spock":  "spock smashes scissors",
+}
+
+func (j *Judge) compareMoves(m1, m2 string) (winner rps.WinnerTag, comment string) {
+	if m1 < m2 {
+		comment = moveComments[m1+"-"+m2]
+	} else {
+		comment = moveComments[m2+"-"+m1]
+	}
+	if m1 == m2 {
+		winner = rps.Draw
+		return
+	}
+	if m1 == "rock" && (m2 == "scissors" || m2 == "lizard") {
+		winner = rps.Player1
+		return
+	}
+	if m1 == "paper" && (m2 == "rock" || m2 == "spock") {
+		winner = rps.Player1
+		return
+	}
+	if m1 == "scissors" && (m2 == "paper" || m2 == "lizard") {
+		winner = rps.Player1
+		return
+	}
+	if m1 == "lizard" && (m2 == "paper" || m2 == "spock") {
+		winner = rps.Player1
+		return
+	}
+	if m1 == "spock" && (m2 == "scissors" || m2 == "rock") {
+		winner = rps.Player1
+		return
+	}
+	winner = rps.Player2
+	return
+}
diff --git a/rps/rpsbot/main.go b/rps/rpsbot/main.go
new file mode 100644
index 0000000..56b7c8e
--- /dev/null
+++ b/rps/rpsbot/main.go
@@ -0,0 +1,79 @@
+// Command rpsbot is a binary that runs the fully automated
+// implementation of the RockPaperScissors service, which includes all
+// three roles involved in the game of rock-paper-scissors. It
+// publishes itself as player, judge, and scorekeeper. Then, it
+// initiates games with other players, in a loop. As soon as one game
+// is over, it starts a new one.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"math/rand"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/x/lib/vlog"
+
+	"v.io/core/veyron/lib/signals"
+	_ "v.io/core/veyron/profiles/roaming"
+	sflag "v.io/core/veyron/security/flag"
+
+	"v.io/apps/rps"
+	"v.io/apps/rps/common"
+)
+
+var (
+	name     = flag.String("name", "", "identifier to publish itself as (defaults to user@hostname)")
+	numGames = flag.Int("num-games", -1, "number of games to play (-1 means unlimited)")
+)
+
+func main() {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	auth := sflag.NewAuthorizerOrDie()
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatalf("NewServer failed: %v", err)
+	}
+
+	rand.Seed(time.Now().UnixNano())
+	rpsService := NewRPS(ctx)
+
+	listenSpec := v23.GetListenSpec(ctx)
+	eps, err := server.Listen(listenSpec)
+	if err != nil {
+		vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+	}
+	if *name == "" {
+		*name = common.CreateName()
+	}
+	names := []string{
+		fmt.Sprintf("rps/judge/%s", *name),
+		fmt.Sprintf("rps/player/%s", *name),
+		fmt.Sprintf("rps/scorekeeper/%s", *name),
+	}
+	if err := server.Serve(names[0], rps.RockPaperScissorsServer(rpsService), auth); err != nil {
+		vlog.Fatalf("Serve(%v) failed: %v", names[0], err)
+	}
+	for _, n := range names[1:] {
+		if err := server.AddName(n); err != nil {
+			vlog.Fatalf("(%v) failed: %v", n, err)
+		}
+	}
+	vlog.Infof("Listening on endpoint %s (published as %v)", eps, names)
+
+	go initiateGames(ctx, rpsService)
+	<-signals.ShutdownOnSignals(ctx)
+}
+
+func initiateGames(ctx *context.T, rpsService *RPS) {
+	for i := 0; i < *numGames || *numGames == -1; i++ {
+		if err := rpsService.Player().InitiateGame(ctx); err != nil {
+			vlog.Infof("Failed to initiate game: %v", err)
+		}
+		time.Sleep(5 * time.Second)
+	}
+}
diff --git a/rps/rpsbot/player.go b/rps/rpsbot/player.go
new file mode 100644
index 0000000..1181d25
--- /dev/null
+++ b/rps/rpsbot/player.go
@@ -0,0 +1,155 @@
+package main
+
+import (
+	"math/rand"
+	"time"
+
+	"v.io/apps/rps"
+	"v.io/apps/rps/common"
+	"v.io/core/veyron/lib/stats"
+	"v.io/core/veyron/lib/stats/counter"
+	"v.io/v23/context"
+	"v.io/x/lib/vlog"
+)
+
+type Player struct {
+	gamesPlayed     *counter.Counter
+	gamesWon        *counter.Counter
+	gamesInProgress *stats.Integer
+}
+
+func NewPlayer() *Player {
+	return &Player{
+		gamesPlayed:     stats.NewCounter("player/games-played"),
+		gamesWon:        stats.NewCounter("player/games-won"),
+		gamesInProgress: stats.NewInteger("player/games-in-progress"),
+	}
+}
+
+func (p *Player) Stats() (played, won int64) {
+	played = p.gamesPlayed.Value()
+	won = p.gamesWon.Value()
+	return
+}
+
+// only used by tests.
+func (p *Player) WaitUntilIdle() {
+	for p.gamesInProgress.Value() != int64(0) {
+		time.Sleep(10 * time.Millisecond)
+	}
+}
+
+func (p *Player) InitiateGame(ctx *context.T) error {
+	judge, err := common.FindJudge(ctx)
+	if err != nil {
+		vlog.Infof("FindJudge: %v", err)
+		return err
+	}
+	gameID, gameOpts, err := p.createGame(ctx, judge)
+	if err != nil {
+		vlog.Infof("createGame: %v", err)
+		return err
+	}
+	vlog.VI(1).Infof("Created gameID %q on %q", gameID, judge)
+
+	for {
+		opponent, err := common.FindPlayer(ctx)
+		if err != nil {
+			vlog.Infof("FindPlayer: %v", err)
+			return err
+		}
+		vlog.VI(1).Infof("chosen opponent is %q", opponent)
+		if err = p.sendChallenge(ctx, opponent, judge, gameID, gameOpts); err == nil {
+			break
+		}
+		vlog.Infof("sendChallenge: %v", err)
+	}
+	result, err := p.playGame(ctx, judge, gameID)
+	if err != nil {
+		vlog.Infof("playGame: %v", err)
+		return err
+	}
+	if result.YouWon {
+		vlog.VI(1).Info("Game result: I won! :)")
+	} else {
+		vlog.VI(1).Info("Game result: I lost :(")
+	}
+	return nil
+}
+
+func (p *Player) createGame(ctx *context.T, server string) (rps.GameID, rps.GameOptions, error) {
+	j := rps.RockPaperScissorsClient(server)
+	numRounds := 3 + rand.Intn(3)
+	gameType := rps.Classic
+	if rand.Intn(2) == 1 {
+		gameType = rps.LizardSpock
+	}
+	gameOpts := rps.GameOptions{NumRounds: int32(numRounds), GameType: gameType}
+	gameId, err := j.CreateGame(ctx, gameOpts)
+	return gameId, gameOpts, err
+}
+
+func (p *Player) sendChallenge(ctx *context.T, opponent, judge string, gameID rps.GameID, gameOpts rps.GameOptions) error {
+	o := rps.RockPaperScissorsClient(opponent)
+	return o.Challenge(ctx, judge, gameID, gameOpts)
+}
+
+// challenge receives an incoming challenge and starts to play a new game.
+// Note that the new game will occur in a new context.
+func (p *Player) challenge(ctx *context.T, judge string, gameID rps.GameID, _ rps.GameOptions) error {
+	vlog.VI(1).Infof("challenge received: %s %v", judge, gameID)
+	go p.playGame(ctx, judge, gameID)
+	return nil
+}
+
+// playGame plays an entire game, which really only consists of reading
+// commands from the server, and picking a random "move" when asked to.
+func (p *Player) playGame(outer *context.T, judge string, gameID rps.GameID) (rps.PlayResult, error) {
+	ctx, cancel := context.WithTimeout(outer, 10*time.Minute)
+	defer cancel()
+	p.gamesInProgress.Incr(1)
+	defer p.gamesInProgress.Incr(-1)
+	j := rps.RockPaperScissorsClient(judge)
+	game, err := j.Play(ctx, gameID)
+	if err != nil {
+		return rps.PlayResult{}, err
+	}
+	rStream := game.RecvStream()
+	sender := game.SendStream()
+	for rStream.Advance() {
+		in := rStream.Value()
+		switch v := in.(type) {
+		case rps.JudgeActionPlayerNum:
+			vlog.VI(1).Infof("I'm player %d", v.Value)
+		case rps.JudgeActionOpponentName:
+			vlog.VI(1).Infof("My opponent is %q", v.Value)
+		case rps.JudgeActionMoveOptions:
+			opts := v.Value
+			n := rand.Intn(len(opts))
+			vlog.VI(1).Infof("My turn to play. Picked %q from %v", opts[n], opts)
+			if err := sender.Send(rps.PlayerActionMove{opts[n]}); err != nil {
+				return rps.PlayResult{}, err
+			}
+		case rps.JudgeActionRoundResult:
+			rr := v.Value
+			vlog.VI(1).Infof("Player 1 played %q. Player 2 played %q. Winner: %v %s",
+				rr.Moves[0], rr.Moves[1], rr.Winner, rr.Comment)
+		case rps.JudgeActionScore:
+			vlog.VI(1).Infof("Score card: %s", common.FormatScoreCard(v.Value))
+		default:
+			vlog.Infof("unexpected message type: %T", in)
+		}
+	}
+
+	if err := rStream.Err(); err != nil {
+		vlog.Infof("stream error: %v", err)
+	} else {
+		vlog.VI(1).Infof("Game Ended")
+	}
+	result, err := game.Finish()
+	p.gamesPlayed.Incr(1)
+	if err == nil && result.YouWon {
+		p.gamesWon.Incr(1)
+	}
+	return result, err
+}
diff --git a/rps/rpsbot/scorekeeper.go b/rps/rpsbot/scorekeeper.go
new file mode 100644
index 0000000..6fad0aa
--- /dev/null
+++ b/rps/rpsbot/scorekeeper.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+	"v.io/apps/rps"
+	"v.io/apps/rps/common"
+	"v.io/core/veyron/lib/stats"
+	"v.io/core/veyron/lib/stats/counter"
+	"v.io/v23/ipc"
+	"v.io/x/lib/vlog"
+)
+
+type ScoreKeeper struct {
+	numRecords *counter.Counter
+}
+
+func NewScoreKeeper() *ScoreKeeper {
+	return &ScoreKeeper{
+		numRecords: stats.NewCounter("scorekeeper/num-records"),
+	}
+}
+
+func (k *ScoreKeeper) Stats() int64 {
+	return k.numRecords.Value()
+}
+
+func (k *ScoreKeeper) Record(ctx ipc.ServerContext, score rps.ScoreCard) error {
+	b, _ := ctx.RemoteBlessings().ForContext(ctx)
+	vlog.VI(1).Infof("Received ScoreCard from %v:", b)
+	vlog.VI(1).Info(common.FormatScoreCard(score))
+	k.numRecords.Incr(1)
+	return nil
+}
diff --git a/rps/rpsplayer/main.go b/rps/rpsplayer/main.go
new file mode 100644
index 0000000..2bba584
--- /dev/null
+++ b/rps/rpsplayer/main.go
@@ -0,0 +1,308 @@
+// Command rpsplayer is a command-line implementation of the Player
+// service that allows a human player to join the game.
+package main
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"sort"
+	"strings"
+	"sync"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/vtrace"
+	"v.io/x/lib/vlog"
+
+	_ "v.io/core/veyron/profiles/roaming"
+	sflag "v.io/core/veyron/security/flag"
+
+	"v.io/apps/rps"
+	"v.io/apps/rps/common"
+)
+
+var (
+	name = flag.String("name", "", "identifier to publish itself as (defaults to user@hostname)")
+)
+
+func main() {
+	rootctx, shutdown := v23.Init()
+	defer shutdown()
+
+	for {
+		ctx, _ := vtrace.SetNewTrace(rootctx)
+		if selectOne([]string{"Initiate Game", "Wait For Challenge"}) == 0 {
+			initiateGame(ctx)
+		} else {
+			fmt.Println("Waiting to receive a challenge...")
+			game := recvChallenge(ctx)
+			playGame(ctx, game.address, game.id)
+		}
+		if selectOne([]string{"Play Again", "Quit"}) == 1 {
+			break
+		}
+	}
+}
+
+type gameChallenge struct {
+	address string
+	id      rps.GameID
+	opts    rps.GameOptions
+}
+
+// impl is a PlayerServerMethods implementation that prompts the user to accept
+// or decline challenges. While waiting for a reply from the user, any incoming
+// challenges are auto-declined.
+type impl struct {
+	ch      chan gameChallenge
+	decline bool
+	lock    sync.Mutex
+}
+
+func (i *impl) setDecline(v bool) bool {
+	i.lock.Lock()
+	defer i.lock.Unlock()
+	prev := i.decline
+	i.decline = v
+	return prev
+}
+
+func (i *impl) Challenge(ctx ipc.ServerContext, address string, id rps.GameID, opts rps.GameOptions) error {
+	remote, _ := ctx.RemoteBlessings().ForContext(ctx)
+	vlog.VI(1).Infof("Challenge (%q, %+v) from %v", address, id, remote)
+	// When setDecline(true) returns, future challenges will be declined.
+	// Whether the current challenge should be considered depends on the
+	// previous state. If 'decline' was already true, we need to decline
+	// this challenge. It 'decline' was false, this is the first challenge
+	// that we should process.
+	if i.setDecline(true) {
+		return errors.New("player is busy")
+	}
+	fmt.Println()
+	fmt.Printf("Challenge received from %v for a %d-round ", remote, opts.NumRounds)
+	switch opts.GameType {
+	case rps.Classic:
+		fmt.Print("Classic ")
+	case rps.LizardSpock:
+		fmt.Print("Lizard-Spock ")
+	default:
+	}
+	fmt.Println("Game.")
+	if selectOne([]string{"Accept", "Decline"}) == 0 {
+		i.ch <- gameChallenge{address, id, opts}
+		return nil
+	}
+	// Start considering challenges again.
+	i.setDecline(false)
+	return errors.New("player declined challenge")
+}
+
+// recvChallenge runs a server until a game challenge is accepted by the user.
+// The server is stopped afterwards.
+func recvChallenge(ctx *context.T) gameChallenge {
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatalf("NewServer failed: %v", err)
+	}
+	ch := make(chan gameChallenge)
+
+	listenSpec := v23.GetListenSpec(ctx)
+	ep, err := server.Listen(listenSpec)
+	if err != nil {
+		vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+	}
+	if *name == "" {
+		*name = common.CreateName()
+	}
+	if err := server.Serve(fmt.Sprintf("rps/player/%s", *name), rps.PlayerServer(&impl{ch: ch}), sflag.NewAuthorizerOrDie()); err != nil {
+		vlog.Fatalf("Serve failed: %v", err)
+	}
+	vlog.Infof("Listening on endpoint /%s", ep)
+	result := <-ch
+	server.Stop()
+	return result
+}
+
+// initiateGame initiates a new game by getting a list of judges and players,
+// and asking the user to select one of each, to select the game options, what
+// to play, etc.
+func initiateGame(ctx *context.T) error {
+	jChan := make(chan []string)
+	oChan := make(chan []string)
+	go findAll(ctx, "judge", jChan)
+	go findAll(ctx, "player", oChan)
+
+	fmt.Println("Looking for available participants...")
+	judges := <-jChan
+	opponents := <-oChan
+	fmt.Println()
+	if len(judges) == 0 || len(opponents) == 0 {
+		return errors.New("no one to play with")
+	}
+
+	fmt.Println("Choose a judge:")
+	j := selectOne(judges)
+	fmt.Println("Choose an opponent:")
+	o := selectOne(opponents)
+	fmt.Println("Choose the type of rock-paper-scissors game would you like to play:")
+	gameType := selectOne([]string{"Classic", "LizardSpock"})
+	fmt.Println("Choose the number of rounds required to win:")
+	numRounds := selectOne([]string{"1", "2", "3", "4", "5", "6"}) + 1
+	gameOpts := rps.GameOptions{NumRounds: int32(numRounds), GameType: rps.GameTypeTag(gameType)}
+
+	gameID, err := createGame(ctx, judges[j], gameOpts)
+	if err != nil {
+		vlog.Infof("createGame: %v", err)
+		return err
+	}
+	for {
+		err := sendChallenge(ctx, opponents[o], judges[j], gameID, gameOpts)
+		if err == nil {
+			break
+		}
+		fmt.Printf("Challenge was declined by %s (%v)\n", opponents[o], err)
+		fmt.Println("Choose another opponent:")
+		o = selectOne(opponents)
+	}
+	fmt.Println("Joining the game...")
+
+	if _, err = playGame(ctx, judges[j], gameID); err != nil {
+		vlog.Infof("playGame: %v", err)
+		return err
+	}
+	return nil
+}
+
+func createGame(ctx *context.T, server string, opts rps.GameOptions) (rps.GameID, error) {
+	j := rps.RockPaperScissorsClient(server)
+	return j.CreateGame(ctx, opts)
+}
+
+func sendChallenge(ctx *context.T, opponent, judge string, gameID rps.GameID, gameOpts rps.GameOptions) error {
+	o := rps.RockPaperScissorsClient(opponent)
+	return o.Challenge(ctx, judge, gameID, gameOpts)
+}
+
+func playGame(outer *context.T, judge string, gameID rps.GameID) (rps.PlayResult, error) {
+	ctx, cancel := context.WithTimeout(outer, 10*time.Minute)
+	defer cancel()
+	fmt.Println()
+	j := rps.RockPaperScissorsClient(judge)
+	game, err := j.Play(ctx, gameID)
+	if err != nil {
+		return rps.PlayResult{}, err
+	}
+	var playerNum int32
+	rStream := game.RecvStream()
+	for rStream.Advance() {
+		in := rStream.Value()
+		switch v := in.(type) {
+		case rps.JudgeActionPlayerNum:
+			playerNum = v.Value
+			fmt.Printf("You are player %d\n", playerNum)
+		case rps.JudgeActionOpponentName:
+			fmt.Printf("Your opponent is %q\n", v.Value)
+		case rps.JudgeActionRoundResult:
+			rr := v.Value
+			if playerNum != 1 && playerNum != 2 {
+				vlog.Fatalf("invalid playerNum: %d", playerNum)
+			}
+			fmt.Printf("You played %q\n", rr.Moves[playerNum-1])
+			fmt.Printf("Your opponent played %q\n", rr.Moves[2-playerNum])
+			if len(rr.Comment) > 0 {
+				fmt.Printf(">>> %s <<<\n", strings.ToUpper(rr.Comment))
+			}
+			if rr.Winner == 0 {
+				fmt.Println("----- It's a draw -----")
+			} else if rps.WinnerTag(playerNum) == rr.Winner {
+				fmt.Println("***** You WIN *****")
+			} else {
+				fmt.Println("##### You LOSE #####")
+			}
+		case rps.JudgeActionMoveOptions:
+			opts := v.Value
+			fmt.Println()
+			fmt.Println("Choose your weapon:")
+			m := selectOne(opts)
+			if err := game.SendStream().Send(rps.PlayerActionMove{opts[m]}); err != nil {
+				return rps.PlayResult{}, err
+			}
+		case rps.JudgeActionScore:
+			score := v.Value
+			fmt.Println()
+			fmt.Println("==== GAME SUMMARY ====")
+			fmt.Print(common.FormatScoreCard(score))
+			fmt.Println("======================")
+			if rps.WinnerTag(playerNum) == score.Winner {
+				fmt.Println("You won! :)")
+			} else {
+				fmt.Println("You lost! :(")
+			}
+		default:
+			vlog.Infof("unexpected message type: %T", in)
+		}
+	}
+	if err := rStream.Err(); err == nil {
+		fmt.Println("Game Ended")
+	} else {
+		vlog.Infof("stream error: %v", err)
+	}
+
+	return game.Finish()
+}
+
+func selectOne(choices []string) (choice int) {
+	if len(choices) == 0 {
+		vlog.Fatal("No options to choose from!")
+	}
+	fmt.Println()
+	for i, x := range choices {
+		fmt.Printf("  %d. %s\n", i+1, x)
+	}
+	fmt.Println()
+	for {
+		if len(choices) == 1 {
+			fmt.Print("Select one [1] --> ")
+		} else {
+			fmt.Printf("Select one [1-%d] --> ", len(choices))
+		}
+		fmt.Scanf("%d", &choice)
+		if choice >= 1 && choice <= len(choices) {
+			choice -= 1
+			break
+		}
+	}
+	fmt.Println()
+	return
+}
+
+func findAll(ctx *context.T, t string, out chan []string) {
+	ns := v23.GetNamespace(ctx)
+	var result []string
+	c, err := ns.Glob(ctx, "rps/"+t+"/*")
+	if err != nil {
+		vlog.Infof("ns.Glob failed: %v", err)
+		out <- result
+		return
+	}
+	for e := range c {
+		switch v := e.(type) {
+		case *naming.GlobError:
+			fmt.Print("E")
+		case *naming.MountEntry:
+			fmt.Print(".")
+			result = append(result, v.Name)
+		}
+	}
+	if len(result) == 0 {
+		vlog.Infof("found no %ss", t)
+		out <- result
+		return
+	}
+	sort.Strings(result)
+	out <- result
+}
diff --git a/rps/rpsscorekeeper/main.go b/rps/rpsscorekeeper/main.go
new file mode 100644
index 0000000..4533eb5
--- /dev/null
+++ b/rps/rpsscorekeeper/main.go
@@ -0,0 +1,63 @@
+// Command rpsscorekeeper is a simple implementation of the
+// ScoreKeeper service. It publishes itself as a score keeper for the
+// rock-paper-scissors game and prints out all the score cards it
+// receives to stdout.
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/ipc"
+	"v.io/x/lib/vlog"
+
+	_ "v.io/core/veyron/profiles/roaming"
+	sflag "v.io/core/veyron/security/flag"
+
+	"v.io/apps/rps"
+	"v.io/apps/rps/common"
+)
+
+type impl struct {
+	ch chan rps.ScoreCard
+}
+
+func (i *impl) Record(ctx ipc.ServerContext, score rps.ScoreCard) error {
+	b, _ := ctx.RemoteBlessings().ForContext(ctx)
+	vlog.VI(1).Infof("Record (%+v) from %v", score, b)
+	i.ch <- score
+	return nil
+}
+
+func main() {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatalf("NewServer failed: %v", err)
+	}
+	defer server.Stop()
+
+	ch := make(chan rps.ScoreCard)
+	rpsService := &impl{ch}
+
+	listenSpec := v23.GetListenSpec(ctx)
+	ep, err := server.Listen(listenSpec)
+	if err != nil {
+		vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+	}
+	hostname, err := os.Hostname()
+	if err != nil {
+		vlog.Fatalf("os.Hostname failed: %v", err)
+	}
+	if err := server.Serve(fmt.Sprintf("rps/scorekeeper/%s", hostname), rps.ScoreKeeperServer(rpsService), sflag.NewAuthorizerOrDie()); err != nil {
+		vlog.Fatalf("Serve failed: %v", err)
+	}
+	vlog.Infof("Listening on endpoint /%s", ep)
+
+	for score := range ch {
+		fmt.Print("======================\n", common.FormatScoreCard(score))
+	}
+}
diff --git a/rps/service.vdl b/rps/service.vdl
new file mode 100644
index 0000000..708cb85
--- /dev/null
+++ b/rps/service.vdl
@@ -0,0 +1,110 @@
+// Package rps is an example of veyron service for playing the game of
+// Rock-Paper-Scissors. (http://en.wikipedia.org/wiki/Rock-paper-scissors)
+//
+// There are three different roles in the game:
+//
+// 1. Judge: A judge enforces the rules of the game and decides who
+//    the winner is. At the end of the game, the judge reports the
+//    final score to all the score keepers.
+//
+// 2. Player: A player can ask a judge to start a new game, it can
+//    challenge another player, and it can play a game.
+//
+// 3. ScoreKeeper: A score keeper receives the final score for a game
+//    after it ended.
+package rps
+
+import "v.io/v23/services/security/access"
+
+type RockPaperScissors interface {
+  Judge
+  Player
+  ScoreKeeper
+}
+
+type Judge interface {
+  // CreateGame creates a new game with the given game options and returns a game
+  // identifier that can be used by the players to join the game.
+  CreateGame(Opts GameOptions) (GameID | error) {access.Write}
+  // Play lets a player join an existing game and play.
+  Play(ID GameID) stream<PlayerAction,JudgeAction> (PlayResult | error) {access.Write}
+}
+
+// A GameID is used to uniquely identify a game within one Judge.
+type GameID struct {
+  ID string
+}
+
+// GameOptions specifies the parameters of a game.
+type GameOptions struct {
+  NumRounds int32        // The number of rounds that a player must win to win the game.
+  GameType  GameTypeTag  // The type of game to play: Classic or LizardSpock.
+}
+
+type GameTypeTag byte
+const (
+  Classic     = GameTypeTag(0)  // Rock-Paper-Scissors
+  LizardSpock = GameTypeTag(1)  // Rock-Paper-Scissors-Lizard-Spock
+)
+
+type PlayerAction union {
+  Move string  // The move that the player wants to make.
+  Quit unused  // Indicates that the player is quitting the game.
+}
+
+type unused struct{}
+
+type JudgeAction union {
+  PlayerNum    int32      // The player's number.
+  OpponentName string     // The name of the opponent.
+  MoveOptions  []string   // A list of allowed moves that the player must choose from.
+  RoundResult  Round      // The result of the previous round.
+  Score        ScoreCard  // The result of the game.
+}
+
+type PlayersMoves [2]string
+
+// Round represents the state of a round.
+type Round struct {
+  Moves       PlayersMoves  // Each player's move.
+  Comment     string        // A text comment from judge about the round.
+  Winner      WinnerTag     // Who won the round.
+  StartTimeNS int64         // The time at which the round started.
+  EndTimeNS   int64         // The time at which the round ended.
+}
+
+// WinnerTag is a type used to indicate whether a round or a game was a draw,
+// was won by player 1 or was won by player 2.
+type WinnerTag byte
+const (
+  Draw    = WinnerTag(0)
+  Player1 = WinnerTag(1)
+  Player2 = WinnerTag(2)
+)
+
+// PlayResult is the value returned by the Play method. It indicates the outcome of the game.
+type PlayResult struct {
+  YouWon bool  // True if the player receiving the result won the game.
+}
+
+// Player can receive challenges from other players.
+type Player interface {
+  // Challenge is used by other players to challenge this player to a game. If
+  // the challenge is accepted, the method returns nil.
+  Challenge(Address string, ID GameID, Opts GameOptions) error {access.Write}
+}
+
+// ScoreKeeper receives the outcome of games from Judges.
+type ScoreKeeper interface {
+  Record(Score ScoreCard) error {access.Write}
+}
+
+type ScoreCard struct {
+  Opts        GameOptions // The game options.
+  Judge       string      // The name of the judge.
+  Players     []string    // The name of the players.
+  Rounds      []Round     // The outcome of each round.
+  StartTimeNS int64       // The time at which the game started.
+  EndTimeNS   int64       // The time at which the game ended.
+  Winner      WinnerTag   // Who won the game.
+}
diff --git a/rps/service.vdl.go b/rps/service.vdl.go
new file mode 100644
index 0000000..49d8f17
--- /dev/null
+++ b/rps/service.vdl.go
@@ -0,0 +1,955 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: service.vdl
+
+// Package rps is an example of veyron service for playing the game of
+// Rock-Paper-Scissors. (http://en.wikipedia.org/wiki/Rock-paper-scissors)
+//
+// There are three different roles in the game:
+//
+// 1. Judge: A judge enforces the rules of the game and decides who
+//    the winner is. At the end of the game, the judge reports the
+//    final score to all the score keepers.
+//
+// 2. Player: A player can ask a judge to start a new game, it can
+//    challenge another player, and it can play a game.
+//
+// 3. ScoreKeeper: A score keeper receives the final score for a game
+//    after it ended.
+package rps
+
+import (
+	// VDL system imports
+	"io"
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/v23/services/security/access"
+)
+
+// A GameID is used to uniquely identify a game within one Judge.
+type GameID struct {
+	ID string
+}
+
+func (GameID) __VDLReflect(struct {
+	Name string "v.io/apps/rps.GameID"
+}) {
+}
+
+// GameOptions specifies the parameters of a game.
+type GameOptions struct {
+	NumRounds int32       // The number of rounds that a player must win to win the game.
+	GameType  GameTypeTag // The type of game to play: Classic or LizardSpock.
+}
+
+func (GameOptions) __VDLReflect(struct {
+	Name string "v.io/apps/rps.GameOptions"
+}) {
+}
+
+type GameTypeTag byte
+
+func (GameTypeTag) __VDLReflect(struct {
+	Name string "v.io/apps/rps.GameTypeTag"
+}) {
+}
+
+type (
+	// PlayerAction represents any single field of the PlayerAction union type.
+	PlayerAction interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the PlayerAction union type.
+		__VDLReflect(__PlayerActionReflect)
+	}
+	// PlayerActionMove represents field Move of the PlayerAction union type.
+	PlayerActionMove struct{ Value string } // The move that the player wants to make.
+	// PlayerActionQuit represents field Quit of the PlayerAction union type.
+	PlayerActionQuit struct{ Value unused } // Indicates that the player is quitting the game.
+	// __PlayerActionReflect describes the PlayerAction union type.
+	__PlayerActionReflect struct {
+		Name  string "v.io/apps/rps.PlayerAction"
+		Type  PlayerAction
+		Union struct {
+			Move PlayerActionMove
+			Quit PlayerActionQuit
+		}
+	}
+)
+
+func (x PlayerActionMove) Index() int                         { return 0 }
+func (x PlayerActionMove) Interface() interface{}             { return x.Value }
+func (x PlayerActionMove) Name() string                       { return "Move" }
+func (x PlayerActionMove) __VDLReflect(__PlayerActionReflect) {}
+
+func (x PlayerActionQuit) Index() int                         { return 1 }
+func (x PlayerActionQuit) Interface() interface{}             { return x.Value }
+func (x PlayerActionQuit) Name() string                       { return "Quit" }
+func (x PlayerActionQuit) __VDLReflect(__PlayerActionReflect) {}
+
+type unused struct {
+}
+
+func (unused) __VDLReflect(struct {
+	Name string "v.io/apps/rps.unused"
+}) {
+}
+
+type (
+	// JudgeAction represents any single field of the JudgeAction union type.
+	JudgeAction interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the JudgeAction union type.
+		__VDLReflect(__JudgeActionReflect)
+	}
+	// JudgeActionPlayerNum represents field PlayerNum of the JudgeAction union type.
+	JudgeActionPlayerNum struct{ Value int32 } // The player's number.
+	// JudgeActionOpponentName represents field OpponentName of the JudgeAction union type.
+	JudgeActionOpponentName struct{ Value string } // The name of the opponent.
+	// JudgeActionMoveOptions represents field MoveOptions of the JudgeAction union type.
+	JudgeActionMoveOptions struct{ Value []string } // A list of allowed moves that the player must choose from.
+	// JudgeActionRoundResult represents field RoundResult of the JudgeAction union type.
+	JudgeActionRoundResult struct{ Value Round } // The result of the previous round.
+	// JudgeActionScore represents field Score of the JudgeAction union type.
+	JudgeActionScore struct{ Value ScoreCard } // The result of the game.
+	// __JudgeActionReflect describes the JudgeAction union type.
+	__JudgeActionReflect struct {
+		Name  string "v.io/apps/rps.JudgeAction"
+		Type  JudgeAction
+		Union struct {
+			PlayerNum    JudgeActionPlayerNum
+			OpponentName JudgeActionOpponentName
+			MoveOptions  JudgeActionMoveOptions
+			RoundResult  JudgeActionRoundResult
+			Score        JudgeActionScore
+		}
+	}
+)
+
+func (x JudgeActionPlayerNum) Index() int                        { return 0 }
+func (x JudgeActionPlayerNum) Interface() interface{}            { return x.Value }
+func (x JudgeActionPlayerNum) Name() string                      { return "PlayerNum" }
+func (x JudgeActionPlayerNum) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionOpponentName) Index() int                        { return 1 }
+func (x JudgeActionOpponentName) Interface() interface{}            { return x.Value }
+func (x JudgeActionOpponentName) Name() string                      { return "OpponentName" }
+func (x JudgeActionOpponentName) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionMoveOptions) Index() int                        { return 2 }
+func (x JudgeActionMoveOptions) Interface() interface{}            { return x.Value }
+func (x JudgeActionMoveOptions) Name() string                      { return "MoveOptions" }
+func (x JudgeActionMoveOptions) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionRoundResult) Index() int                        { return 3 }
+func (x JudgeActionRoundResult) Interface() interface{}            { return x.Value }
+func (x JudgeActionRoundResult) Name() string                      { return "RoundResult" }
+func (x JudgeActionRoundResult) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionScore) Index() int                        { return 4 }
+func (x JudgeActionScore) Interface() interface{}            { return x.Value }
+func (x JudgeActionScore) Name() string                      { return "Score" }
+func (x JudgeActionScore) __VDLReflect(__JudgeActionReflect) {}
+
+type PlayersMoves [2]string
+
+func (PlayersMoves) __VDLReflect(struct {
+	Name string "v.io/apps/rps.PlayersMoves"
+}) {
+}
+
+// Round represents the state of a round.
+type Round struct {
+	Moves       PlayersMoves // Each player's move.
+	Comment     string       // A text comment from judge about the round.
+	Winner      WinnerTag    // Who won the round.
+	StartTimeNS int64        // The time at which the round started.
+	EndTimeNS   int64        // The time at which the round ended.
+}
+
+func (Round) __VDLReflect(struct {
+	Name string "v.io/apps/rps.Round"
+}) {
+}
+
+// WinnerTag is a type used to indicate whether a round or a game was a draw,
+// was won by player 1 or was won by player 2.
+type WinnerTag byte
+
+func (WinnerTag) __VDLReflect(struct {
+	Name string "v.io/apps/rps.WinnerTag"
+}) {
+}
+
+// PlayResult is the value returned by the Play method. It indicates the outcome of the game.
+type PlayResult struct {
+	YouWon bool // True if the player receiving the result won the game.
+}
+
+func (PlayResult) __VDLReflect(struct {
+	Name string "v.io/apps/rps.PlayResult"
+}) {
+}
+
+type ScoreCard struct {
+	Opts        GameOptions // The game options.
+	Judge       string      // The name of the judge.
+	Players     []string    // The name of the players.
+	Rounds      []Round     // The outcome of each round.
+	StartTimeNS int64       // The time at which the game started.
+	EndTimeNS   int64       // The time at which the game ended.
+	Winner      WinnerTag   // Who won the game.
+}
+
+func (ScoreCard) __VDLReflect(struct {
+	Name string "v.io/apps/rps.ScoreCard"
+}) {
+}
+
+func init() {
+	vdl.Register((*GameID)(nil))
+	vdl.Register((*GameOptions)(nil))
+	vdl.Register((*GameTypeTag)(nil))
+	vdl.Register((*PlayerAction)(nil))
+	vdl.Register((*unused)(nil))
+	vdl.Register((*JudgeAction)(nil))
+	vdl.Register((*PlayersMoves)(nil))
+	vdl.Register((*Round)(nil))
+	vdl.Register((*WinnerTag)(nil))
+	vdl.Register((*PlayResult)(nil))
+	vdl.Register((*ScoreCard)(nil))
+}
+
+const Classic = GameTypeTag(0) // Rock-Paper-Scissors
+
+const LizardSpock = GameTypeTag(1) // Rock-Paper-Scissors-Lizard-Spock
+
+const Draw = WinnerTag(0)
+
+const Player1 = WinnerTag(1)
+
+const Player2 = WinnerTag(2)
+
+// JudgeClientMethods is the client interface
+// containing Judge methods.
+type JudgeClientMethods interface {
+	// CreateGame creates a new game with the given game options and returns a game
+	// identifier that can be used by the players to join the game.
+	CreateGame(ctx *context.T, Opts GameOptions, opts ...ipc.CallOpt) (GameID, error)
+	// Play lets a player join an existing game and play.
+	Play(ctx *context.T, ID GameID, opts ...ipc.CallOpt) (JudgePlayCall, error)
+}
+
+// JudgeClientStub adds universal methods to JudgeClientMethods.
+type JudgeClientStub interface {
+	JudgeClientMethods
+	ipc.UniversalServiceMethods
+}
+
+// JudgeClient returns a client stub for Judge.
+func JudgeClient(name string, opts ...ipc.BindOpt) JudgeClientStub {
+	var client ipc.Client
+	for _, opt := range opts {
+		if clientOpt, ok := opt.(ipc.Client); ok {
+			client = clientOpt
+		}
+	}
+	return implJudgeClientStub{name, client}
+}
+
+type implJudgeClientStub struct {
+	name   string
+	client ipc.Client
+}
+
+func (c implJudgeClientStub) c(ctx *context.T) ipc.Client {
+	if c.client != nil {
+		return c.client
+	}
+	return v23.GetClient(ctx)
+}
+
+func (c implJudgeClientStub) CreateGame(ctx *context.T, i0 GameOptions, opts ...ipc.CallOpt) (o0 GameID, err error) {
+	var call ipc.Call
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "CreateGame", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implJudgeClientStub) Play(ctx *context.T, i0 GameID, opts ...ipc.CallOpt) (ocall JudgePlayCall, err error) {
+	var call ipc.Call
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "Play", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	ocall = &implJudgePlayCall{Call: call}
+	return
+}
+
+// JudgePlayClientStream is the client stream for Judge.Play.
+type JudgePlayClientStream interface {
+	// RecvStream returns the receiver side of the Judge.Play client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() JudgeAction
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Judge.Play client stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors
+		// encountered while sending, or if Send is called after Close or
+		// the stream has been canceled.  Blocks if there is no buffer
+		// space; will unblock when buffer space is available or after
+		// the stream has been canceled.
+		Send(item PlayerAction) error
+		// Close indicates to the server that no more items will be sent;
+		// server Recv calls will receive io.EOF after all sent items.
+		// This is an optional call - e.g. a client might call Close if it
+		// needs to continue receiving items from the server after it's
+		// done sending.  Returns errors encountered while closing, or if
+		// Close is called after the stream has been canceled.  Like Send,
+		// blocks if there is no buffer space available.
+		Close() error
+	}
+}
+
+// JudgePlayCall represents the call returned from Judge.Play.
+type JudgePlayCall interface {
+	JudgePlayClientStream
+	// Finish performs the equivalent of SendStream().Close, then blocks until
+	// the server is done, and returns the positional return values for the call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() (PlayResult, error)
+}
+
+type implJudgePlayCall struct {
+	ipc.Call
+	valRecv JudgeAction
+	errRecv error
+}
+
+func (c *implJudgePlayCall) RecvStream() interface {
+	Advance() bool
+	Value() JudgeAction
+	Err() error
+} {
+	return implJudgePlayCallRecv{c}
+}
+
+type implJudgePlayCallRecv struct {
+	c *implJudgePlayCall
+}
+
+func (c implJudgePlayCallRecv) Advance() bool {
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implJudgePlayCallRecv) Value() JudgeAction {
+	return c.c.valRecv
+}
+func (c implJudgePlayCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implJudgePlayCall) SendStream() interface {
+	Send(item PlayerAction) error
+	Close() error
+} {
+	return implJudgePlayCallSend{c}
+}
+
+type implJudgePlayCallSend struct {
+	c *implJudgePlayCall
+}
+
+func (c implJudgePlayCallSend) Send(item PlayerAction) error {
+	return c.c.Send(item)
+}
+func (c implJudgePlayCallSend) Close() error {
+	return c.c.CloseSend()
+}
+func (c *implJudgePlayCall) Finish() (o0 PlayResult, err error) {
+	err = c.Call.Finish(&o0)
+	return
+}
+
+// JudgeServerMethods is the interface a server writer
+// implements for Judge.
+type JudgeServerMethods interface {
+	// CreateGame creates a new game with the given game options and returns a game
+	// identifier that can be used by the players to join the game.
+	CreateGame(ctx ipc.ServerContext, Opts GameOptions) (GameID, error)
+	// Play lets a player join an existing game and play.
+	Play(ctx JudgePlayContext, ID GameID) (PlayResult, error)
+}
+
+// JudgeServerStubMethods is the server interface containing
+// Judge methods, as expected by ipc.Server.
+// The only difference between this interface and JudgeServerMethods
+// is the streaming methods.
+type JudgeServerStubMethods interface {
+	// CreateGame creates a new game with the given game options and returns a game
+	// identifier that can be used by the players to join the game.
+	CreateGame(ctx ipc.ServerContext, Opts GameOptions) (GameID, error)
+	// Play lets a player join an existing game and play.
+	Play(ctx *JudgePlayContextStub, ID GameID) (PlayResult, error)
+}
+
+// JudgeServerStub adds universal methods to JudgeServerStubMethods.
+type JudgeServerStub interface {
+	JudgeServerStubMethods
+	// Describe the Judge interfaces.
+	Describe__() []ipc.InterfaceDesc
+}
+
+// JudgeServer returns a server stub for Judge.
+// It converts an implementation of JudgeServerMethods into
+// an object that may be used by ipc.Server.
+func JudgeServer(impl JudgeServerMethods) JudgeServerStub {
+	stub := implJudgeServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := ipc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := ipc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implJudgeServerStub struct {
+	impl JudgeServerMethods
+	gs   *ipc.GlobState
+}
+
+func (s implJudgeServerStub) CreateGame(ctx ipc.ServerContext, i0 GameOptions) (GameID, error) {
+	return s.impl.CreateGame(ctx, i0)
+}
+
+func (s implJudgeServerStub) Play(ctx *JudgePlayContextStub, i0 GameID) (PlayResult, error) {
+	return s.impl.Play(ctx, i0)
+}
+
+func (s implJudgeServerStub) Globber() *ipc.GlobState {
+	return s.gs
+}
+
+func (s implJudgeServerStub) Describe__() []ipc.InterfaceDesc {
+	return []ipc.InterfaceDesc{JudgeDesc}
+}
+
+// JudgeDesc describes the Judge interface.
+var JudgeDesc ipc.InterfaceDesc = descJudge
+
+// descJudge hides the desc to keep godoc clean.
+var descJudge = ipc.InterfaceDesc{
+	Name:    "Judge",
+	PkgPath: "v.io/apps/rps",
+	Methods: []ipc.MethodDesc{
+		{
+			Name: "CreateGame",
+			Doc:  "// CreateGame creates a new game with the given game options and returns a game\n// identifier that can be used by the players to join the game.",
+			InArgs: []ipc.ArgDesc{
+				{"Opts", ``}, // GameOptions
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"", ``}, // GameID
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+		},
+		{
+			Name: "Play",
+			Doc:  "// Play lets a player join an existing game and play.",
+			InArgs: []ipc.ArgDesc{
+				{"ID", ``}, // GameID
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"", ``}, // PlayResult
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+		},
+	},
+}
+
+// JudgePlayServerStream is the server stream for Judge.Play.
+type JudgePlayServerStream interface {
+	// RecvStream returns the receiver side of the Judge.Play server stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() PlayerAction
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Judge.Play server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item JudgeAction) error
+	}
+}
+
+// JudgePlayContext represents the context passed to Judge.Play.
+type JudgePlayContext interface {
+	ipc.ServerContext
+	JudgePlayServerStream
+}
+
+// JudgePlayContextStub is a wrapper that converts ipc.StreamServerCall into
+// a typesafe stub that implements JudgePlayContext.
+type JudgePlayContextStub struct {
+	ipc.StreamServerCall
+	valRecv PlayerAction
+	errRecv error
+}
+
+// Init initializes JudgePlayContextStub from ipc.StreamServerCall.
+func (s *JudgePlayContextStub) Init(call ipc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Judge.Play server stream.
+func (s *JudgePlayContextStub) RecvStream() interface {
+	Advance() bool
+	Value() PlayerAction
+	Err() error
+} {
+	return implJudgePlayContextRecv{s}
+}
+
+type implJudgePlayContextRecv struct {
+	s *JudgePlayContextStub
+}
+
+func (s implJudgePlayContextRecv) Advance() bool {
+	s.s.errRecv = s.s.Recv(&s.s.valRecv)
+	return s.s.errRecv == nil
+}
+func (s implJudgePlayContextRecv) Value() PlayerAction {
+	return s.s.valRecv
+}
+func (s implJudgePlayContextRecv) Err() error {
+	if s.s.errRecv == io.EOF {
+		return nil
+	}
+	return s.s.errRecv
+}
+
+// SendStream returns the send side of the Judge.Play server stream.
+func (s *JudgePlayContextStub) SendStream() interface {
+	Send(item JudgeAction) error
+} {
+	return implJudgePlayContextSend{s}
+}
+
+type implJudgePlayContextSend struct {
+	s *JudgePlayContextStub
+}
+
+func (s implJudgePlayContextSend) Send(item JudgeAction) error {
+	return s.s.Send(item)
+}
+
+// PlayerClientMethods is the client interface
+// containing Player methods.
+//
+// Player can receive challenges from other players.
+type PlayerClientMethods interface {
+	// Challenge is used by other players to challenge this player to a game. If
+	// the challenge is accepted, the method returns nil.
+	Challenge(ctx *context.T, Address string, ID GameID, Opts GameOptions, opts ...ipc.CallOpt) error
+}
+
+// PlayerClientStub adds universal methods to PlayerClientMethods.
+type PlayerClientStub interface {
+	PlayerClientMethods
+	ipc.UniversalServiceMethods
+}
+
+// PlayerClient returns a client stub for Player.
+func PlayerClient(name string, opts ...ipc.BindOpt) PlayerClientStub {
+	var client ipc.Client
+	for _, opt := range opts {
+		if clientOpt, ok := opt.(ipc.Client); ok {
+			client = clientOpt
+		}
+	}
+	return implPlayerClientStub{name, client}
+}
+
+type implPlayerClientStub struct {
+	name   string
+	client ipc.Client
+}
+
+func (c implPlayerClientStub) c(ctx *context.T) ipc.Client {
+	if c.client != nil {
+		return c.client
+	}
+	return v23.GetClient(ctx)
+}
+
+func (c implPlayerClientStub) Challenge(ctx *context.T, i0 string, i1 GameID, i2 GameOptions, opts ...ipc.CallOpt) (err error) {
+	var call ipc.Call
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "Challenge", []interface{}{i0, i1, i2}, opts...); err != nil {
+		return
+	}
+	err = call.Finish()
+	return
+}
+
+// PlayerServerMethods is the interface a server writer
+// implements for Player.
+//
+// Player can receive challenges from other players.
+type PlayerServerMethods interface {
+	// Challenge is used by other players to challenge this player to a game. If
+	// the challenge is accepted, the method returns nil.
+	Challenge(ctx ipc.ServerContext, Address string, ID GameID, Opts GameOptions) error
+}
+
+// PlayerServerStubMethods is the server interface containing
+// Player methods, as expected by ipc.Server.
+// There is no difference between this interface and PlayerServerMethods
+// since there are no streaming methods.
+type PlayerServerStubMethods PlayerServerMethods
+
+// PlayerServerStub adds universal methods to PlayerServerStubMethods.
+type PlayerServerStub interface {
+	PlayerServerStubMethods
+	// Describe the Player interfaces.
+	Describe__() []ipc.InterfaceDesc
+}
+
+// PlayerServer returns a server stub for Player.
+// It converts an implementation of PlayerServerMethods into
+// an object that may be used by ipc.Server.
+func PlayerServer(impl PlayerServerMethods) PlayerServerStub {
+	stub := implPlayerServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := ipc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := ipc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implPlayerServerStub struct {
+	impl PlayerServerMethods
+	gs   *ipc.GlobState
+}
+
+func (s implPlayerServerStub) Challenge(ctx ipc.ServerContext, i0 string, i1 GameID, i2 GameOptions) error {
+	return s.impl.Challenge(ctx, i0, i1, i2)
+}
+
+func (s implPlayerServerStub) Globber() *ipc.GlobState {
+	return s.gs
+}
+
+func (s implPlayerServerStub) Describe__() []ipc.InterfaceDesc {
+	return []ipc.InterfaceDesc{PlayerDesc}
+}
+
+// PlayerDesc describes the Player interface.
+var PlayerDesc ipc.InterfaceDesc = descPlayer
+
+// descPlayer hides the desc to keep godoc clean.
+var descPlayer = ipc.InterfaceDesc{
+	Name:    "Player",
+	PkgPath: "v.io/apps/rps",
+	Doc:     "// Player can receive challenges from other players.",
+	Methods: []ipc.MethodDesc{
+		{
+			Name: "Challenge",
+			Doc:  "// Challenge is used by other players to challenge this player to a game. If\n// the challenge is accepted, the method returns nil.",
+			InArgs: []ipc.ArgDesc{
+				{"Address", ``}, // string
+				{"ID", ``},      // GameID
+				{"Opts", ``},    // GameOptions
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+		},
+	},
+}
+
+// ScoreKeeperClientMethods is the client interface
+// containing ScoreKeeper methods.
+//
+// ScoreKeeper receives the outcome of games from Judges.
+type ScoreKeeperClientMethods interface {
+	Record(ctx *context.T, Score ScoreCard, opts ...ipc.CallOpt) error
+}
+
+// ScoreKeeperClientStub adds universal methods to ScoreKeeperClientMethods.
+type ScoreKeeperClientStub interface {
+	ScoreKeeperClientMethods
+	ipc.UniversalServiceMethods
+}
+
+// ScoreKeeperClient returns a client stub for ScoreKeeper.
+func ScoreKeeperClient(name string, opts ...ipc.BindOpt) ScoreKeeperClientStub {
+	var client ipc.Client
+	for _, opt := range opts {
+		if clientOpt, ok := opt.(ipc.Client); ok {
+			client = clientOpt
+		}
+	}
+	return implScoreKeeperClientStub{name, client}
+}
+
+type implScoreKeeperClientStub struct {
+	name   string
+	client ipc.Client
+}
+
+func (c implScoreKeeperClientStub) c(ctx *context.T) ipc.Client {
+	if c.client != nil {
+		return c.client
+	}
+	return v23.GetClient(ctx)
+}
+
+func (c implScoreKeeperClientStub) Record(ctx *context.T, i0 ScoreCard, opts ...ipc.CallOpt) (err error) {
+	var call ipc.Call
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "Record", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish()
+	return
+}
+
+// ScoreKeeperServerMethods is the interface a server writer
+// implements for ScoreKeeper.
+//
+// ScoreKeeper receives the outcome of games from Judges.
+type ScoreKeeperServerMethods interface {
+	Record(ctx ipc.ServerContext, Score ScoreCard) error
+}
+
+// ScoreKeeperServerStubMethods is the server interface containing
+// ScoreKeeper methods, as expected by ipc.Server.
+// There is no difference between this interface and ScoreKeeperServerMethods
+// since there are no streaming methods.
+type ScoreKeeperServerStubMethods ScoreKeeperServerMethods
+
+// ScoreKeeperServerStub adds universal methods to ScoreKeeperServerStubMethods.
+type ScoreKeeperServerStub interface {
+	ScoreKeeperServerStubMethods
+	// Describe the ScoreKeeper interfaces.
+	Describe__() []ipc.InterfaceDesc
+}
+
+// ScoreKeeperServer returns a server stub for ScoreKeeper.
+// It converts an implementation of ScoreKeeperServerMethods into
+// an object that may be used by ipc.Server.
+func ScoreKeeperServer(impl ScoreKeeperServerMethods) ScoreKeeperServerStub {
+	stub := implScoreKeeperServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := ipc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := ipc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implScoreKeeperServerStub struct {
+	impl ScoreKeeperServerMethods
+	gs   *ipc.GlobState
+}
+
+func (s implScoreKeeperServerStub) Record(ctx ipc.ServerContext, i0 ScoreCard) error {
+	return s.impl.Record(ctx, i0)
+}
+
+func (s implScoreKeeperServerStub) Globber() *ipc.GlobState {
+	return s.gs
+}
+
+func (s implScoreKeeperServerStub) Describe__() []ipc.InterfaceDesc {
+	return []ipc.InterfaceDesc{ScoreKeeperDesc}
+}
+
+// ScoreKeeperDesc describes the ScoreKeeper interface.
+var ScoreKeeperDesc ipc.InterfaceDesc = descScoreKeeper
+
+// descScoreKeeper hides the desc to keep godoc clean.
+var descScoreKeeper = ipc.InterfaceDesc{
+	Name:    "ScoreKeeper",
+	PkgPath: "v.io/apps/rps",
+	Doc:     "// ScoreKeeper receives the outcome of games from Judges.",
+	Methods: []ipc.MethodDesc{
+		{
+			Name: "Record",
+			InArgs: []ipc.ArgDesc{
+				{"Score", ``}, // ScoreCard
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+		},
+	},
+}
+
+// RockPaperScissorsClientMethods is the client interface
+// containing RockPaperScissors methods.
+type RockPaperScissorsClientMethods interface {
+	JudgeClientMethods
+	// Player can receive challenges from other players.
+	PlayerClientMethods
+	// ScoreKeeper receives the outcome of games from Judges.
+	ScoreKeeperClientMethods
+}
+
+// RockPaperScissorsClientStub adds universal methods to RockPaperScissorsClientMethods.
+type RockPaperScissorsClientStub interface {
+	RockPaperScissorsClientMethods
+	ipc.UniversalServiceMethods
+}
+
+// RockPaperScissorsClient returns a client stub for RockPaperScissors.
+func RockPaperScissorsClient(name string, opts ...ipc.BindOpt) RockPaperScissorsClientStub {
+	var client ipc.Client
+	for _, opt := range opts {
+		if clientOpt, ok := opt.(ipc.Client); ok {
+			client = clientOpt
+		}
+	}
+	return implRockPaperScissorsClientStub{name, client, JudgeClient(name, client), PlayerClient(name, client), ScoreKeeperClient(name, client)}
+}
+
+type implRockPaperScissorsClientStub struct {
+	name   string
+	client ipc.Client
+
+	JudgeClientStub
+	PlayerClientStub
+	ScoreKeeperClientStub
+}
+
+func (c implRockPaperScissorsClientStub) c(ctx *context.T) ipc.Client {
+	if c.client != nil {
+		return c.client
+	}
+	return v23.GetClient(ctx)
+}
+
+// RockPaperScissorsServerMethods is the interface a server writer
+// implements for RockPaperScissors.
+type RockPaperScissorsServerMethods interface {
+	JudgeServerMethods
+	// Player can receive challenges from other players.
+	PlayerServerMethods
+	// ScoreKeeper receives the outcome of games from Judges.
+	ScoreKeeperServerMethods
+}
+
+// RockPaperScissorsServerStubMethods is the server interface containing
+// RockPaperScissors methods, as expected by ipc.Server.
+// The only difference between this interface and RockPaperScissorsServerMethods
+// is the streaming methods.
+type RockPaperScissorsServerStubMethods interface {
+	JudgeServerStubMethods
+	// Player can receive challenges from other players.
+	PlayerServerStubMethods
+	// ScoreKeeper receives the outcome of games from Judges.
+	ScoreKeeperServerStubMethods
+}
+
+// RockPaperScissorsServerStub adds universal methods to RockPaperScissorsServerStubMethods.
+type RockPaperScissorsServerStub interface {
+	RockPaperScissorsServerStubMethods
+	// Describe the RockPaperScissors interfaces.
+	Describe__() []ipc.InterfaceDesc
+}
+
+// RockPaperScissorsServer returns a server stub for RockPaperScissors.
+// It converts an implementation of RockPaperScissorsServerMethods into
+// an object that may be used by ipc.Server.
+func RockPaperScissorsServer(impl RockPaperScissorsServerMethods) RockPaperScissorsServerStub {
+	stub := implRockPaperScissorsServerStub{
+		impl:                  impl,
+		JudgeServerStub:       JudgeServer(impl),
+		PlayerServerStub:      PlayerServer(impl),
+		ScoreKeeperServerStub: ScoreKeeperServer(impl),
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := ipc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := ipc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implRockPaperScissorsServerStub struct {
+	impl RockPaperScissorsServerMethods
+	JudgeServerStub
+	PlayerServerStub
+	ScoreKeeperServerStub
+	gs *ipc.GlobState
+}
+
+func (s implRockPaperScissorsServerStub) Globber() *ipc.GlobState {
+	return s.gs
+}
+
+func (s implRockPaperScissorsServerStub) Describe__() []ipc.InterfaceDesc {
+	return []ipc.InterfaceDesc{RockPaperScissorsDesc, JudgeDesc, PlayerDesc, ScoreKeeperDesc}
+}
+
+// RockPaperScissorsDesc describes the RockPaperScissors interface.
+var RockPaperScissorsDesc ipc.InterfaceDesc = descRockPaperScissors
+
+// descRockPaperScissors hides the desc to keep godoc clean.
+var descRockPaperScissors = ipc.InterfaceDesc{
+	Name:    "RockPaperScissors",
+	PkgPath: "v.io/apps/rps",
+	Embeds: []ipc.EmbedDesc{
+		{"Judge", "v.io/apps/rps", ``},
+		{"Player", "v.io/apps/rps", "// Player can receive challenges from other players."},
+		{"ScoreKeeper", "v.io/apps/rps", "// ScoreKeeper receives the outcome of games from Judges."},
+	},
+}
diff --git a/tunnel/tunnel.vdl b/tunnel/tunnel.vdl
new file mode 100644
index 0000000..73d3e7e
--- /dev/null
+++ b/tunnel/tunnel.vdl
@@ -0,0 +1,49 @@
+// Package tunnel describes a service that can be used to create a
+// network tunnel from the client to the server.
+package tunnel
+
+import "v.io/v23/services/security/access"
+
+type Tunnel interface {
+  // The Forward method is used for network forwarding. All the data sent over
+  // the byte stream is forwarded to the requested network address and all the
+  // data received from that network connection is sent back in the reply
+  // stream.
+  Forward(network, address string) stream<[]byte, []byte> error {access.Admin}
+
+  // The Shell method is used to either run shell commands remotely, or to open
+  // an interactive shell. The data received over the byte stream is sent to the
+  // shell's stdin, and the data received from the shell's stdout and stderr is
+  // sent back in the reply stream. It returns the exit status of the shell
+  // command.
+  Shell(command string, shellOpts ShellOpts) stream<ClientShellPacket, ServerShellPacket> (int32 | error) {access.Admin}
+}
+
+type ShellOpts struct {
+  UsePty      bool       // Whether to open a pseudo-terminal.
+  Environment []string   // Environment variables to pass to the remote shell.
+  WinSize     WindowSize // The size of the window.
+}
+
+type WindowSize struct {
+  Rows, Cols uint16
+}
+
+type ClientShellPacket union {
+  // Bytes going to the shell's stdin.
+  Stdin      []byte
+  // Indicates that stdin should be closed. The presence of this field indicates
+  // EOF. Its actual value is ignored.
+  EOF        unused
+  // A dynamic update of the window size.
+  WinSize    WindowSize
+}
+
+type unused struct {}
+
+type ServerShellPacket union {
+  // Bytes coming from the shell's stdout.
+  Stdout []byte
+  // Bytes coming from the shell's stderr.
+  Stderr []byte
+}
diff --git a/tunnel/tunnel.vdl.go b/tunnel/tunnel.vdl.go
new file mode 100644
index 0000000..e1cbb01
--- /dev/null
+++ b/tunnel/tunnel.vdl.go
@@ -0,0 +1,694 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: tunnel.vdl
+
+// Package tunnel describes a service that can be used to create a
+// network tunnel from the client to the server.
+package tunnel
+
+import (
+	// VDL system imports
+	"io"
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/v23/services/security/access"
+)
+
+type ShellOpts struct {
+	UsePty      bool       // Whether to open a pseudo-terminal.
+	Environment []string   // Environment variables to pass to the remote shell.
+	WinSize     WindowSize // The size of the window.
+}
+
+func (ShellOpts) __VDLReflect(struct {
+	Name string "v.io/apps/tunnel.ShellOpts"
+}) {
+}
+
+type WindowSize struct {
+	Rows uint16
+	Cols uint16
+}
+
+func (WindowSize) __VDLReflect(struct {
+	Name string "v.io/apps/tunnel.WindowSize"
+}) {
+}
+
+type (
+	// ClientShellPacket represents any single field of the ClientShellPacket union type.
+	ClientShellPacket interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the ClientShellPacket union type.
+		__VDLReflect(__ClientShellPacketReflect)
+	}
+	// ClientShellPacketStdin represents field Stdin of the ClientShellPacket union type.
+	//
+	// Bytes going to the shell's stdin.
+	ClientShellPacketStdin struct{ Value []byte }
+	// ClientShellPacketEOF represents field EOF of the ClientShellPacket union type.
+	//
+	// Indicates that stdin should be closed. The presence of this field indicates
+	// EOF. Its actual value is ignored.
+	ClientShellPacketEOF struct{ Value unused }
+	// ClientShellPacketWinSize represents field WinSize of the ClientShellPacket union type.
+	//
+	// A dynamic update of the window size.
+	ClientShellPacketWinSize struct{ Value WindowSize }
+	// __ClientShellPacketReflect describes the ClientShellPacket union type.
+	__ClientShellPacketReflect struct {
+		Name  string "v.io/apps/tunnel.ClientShellPacket"
+		Type  ClientShellPacket
+		Union struct {
+			Stdin   ClientShellPacketStdin
+			EOF     ClientShellPacketEOF
+			WinSize ClientShellPacketWinSize
+		}
+	}
+)
+
+func (x ClientShellPacketStdin) Index() int                              { return 0 }
+func (x ClientShellPacketStdin) Interface() interface{}                  { return x.Value }
+func (x ClientShellPacketStdin) Name() string                            { return "Stdin" }
+func (x ClientShellPacketStdin) __VDLReflect(__ClientShellPacketReflect) {}
+
+func (x ClientShellPacketEOF) Index() int                              { return 1 }
+func (x ClientShellPacketEOF) Interface() interface{}                  { return x.Value }
+func (x ClientShellPacketEOF) Name() string                            { return "EOF" }
+func (x ClientShellPacketEOF) __VDLReflect(__ClientShellPacketReflect) {}
+
+func (x ClientShellPacketWinSize) Index() int                              { return 2 }
+func (x ClientShellPacketWinSize) Interface() interface{}                  { return x.Value }
+func (x ClientShellPacketWinSize) Name() string                            { return "WinSize" }
+func (x ClientShellPacketWinSize) __VDLReflect(__ClientShellPacketReflect) {}
+
+type unused struct {
+}
+
+func (unused) __VDLReflect(struct {
+	Name string "v.io/apps/tunnel.unused"
+}) {
+}
+
+type (
+	// ServerShellPacket represents any single field of the ServerShellPacket union type.
+	ServerShellPacket interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the ServerShellPacket union type.
+		__VDLReflect(__ServerShellPacketReflect)
+	}
+	// ServerShellPacketStdout represents field Stdout of the ServerShellPacket union type.
+	//
+	// Bytes coming from the shell's stdout.
+	ServerShellPacketStdout struct{ Value []byte }
+	// ServerShellPacketStderr represents field Stderr of the ServerShellPacket union type.
+	//
+	// Bytes coming from the shell's stderr.
+	ServerShellPacketStderr struct{ Value []byte }
+	// __ServerShellPacketReflect describes the ServerShellPacket union type.
+	__ServerShellPacketReflect struct {
+		Name  string "v.io/apps/tunnel.ServerShellPacket"
+		Type  ServerShellPacket
+		Union struct {
+			Stdout ServerShellPacketStdout
+			Stderr ServerShellPacketStderr
+		}
+	}
+)
+
+func (x ServerShellPacketStdout) Index() int                              { return 0 }
+func (x ServerShellPacketStdout) Interface() interface{}                  { return x.Value }
+func (x ServerShellPacketStdout) Name() string                            { return "Stdout" }
+func (x ServerShellPacketStdout) __VDLReflect(__ServerShellPacketReflect) {}
+
+func (x ServerShellPacketStderr) Index() int                              { return 1 }
+func (x ServerShellPacketStderr) Interface() interface{}                  { return x.Value }
+func (x ServerShellPacketStderr) Name() string                            { return "Stderr" }
+func (x ServerShellPacketStderr) __VDLReflect(__ServerShellPacketReflect) {}
+
+func init() {
+	vdl.Register((*ShellOpts)(nil))
+	vdl.Register((*WindowSize)(nil))
+	vdl.Register((*ClientShellPacket)(nil))
+	vdl.Register((*unused)(nil))
+	vdl.Register((*ServerShellPacket)(nil))
+}
+
+// TunnelClientMethods is the client interface
+// containing Tunnel methods.
+type TunnelClientMethods interface {
+	// The Forward method is used for network forwarding. All the data sent over
+	// the byte stream is forwarded to the requested network address and all the
+	// data received from that network connection is sent back in the reply
+	// stream.
+	Forward(ctx *context.T, network string, address string, opts ...ipc.CallOpt) (TunnelForwardCall, error)
+	// The Shell method is used to either run shell commands remotely, or to open
+	// an interactive shell. The data received over the byte stream is sent to the
+	// shell's stdin, and the data received from the shell's stdout and stderr is
+	// sent back in the reply stream. It returns the exit status of the shell
+	// command.
+	Shell(ctx *context.T, command string, shellOpts ShellOpts, opts ...ipc.CallOpt) (TunnelShellCall, error)
+}
+
+// TunnelClientStub adds universal methods to TunnelClientMethods.
+type TunnelClientStub interface {
+	TunnelClientMethods
+	ipc.UniversalServiceMethods
+}
+
+// TunnelClient returns a client stub for Tunnel.
+func TunnelClient(name string, opts ...ipc.BindOpt) TunnelClientStub {
+	var client ipc.Client
+	for _, opt := range opts {
+		if clientOpt, ok := opt.(ipc.Client); ok {
+			client = clientOpt
+		}
+	}
+	return implTunnelClientStub{name, client}
+}
+
+type implTunnelClientStub struct {
+	name   string
+	client ipc.Client
+}
+
+func (c implTunnelClientStub) c(ctx *context.T) ipc.Client {
+	if c.client != nil {
+		return c.client
+	}
+	return v23.GetClient(ctx)
+}
+
+func (c implTunnelClientStub) Forward(ctx *context.T, i0 string, i1 string, opts ...ipc.CallOpt) (ocall TunnelForwardCall, err error) {
+	var call ipc.Call
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "Forward", []interface{}{i0, i1}, opts...); err != nil {
+		return
+	}
+	ocall = &implTunnelForwardCall{Call: call}
+	return
+}
+
+func (c implTunnelClientStub) Shell(ctx *context.T, i0 string, i1 ShellOpts, opts ...ipc.CallOpt) (ocall TunnelShellCall, err error) {
+	var call ipc.Call
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "Shell", []interface{}{i0, i1}, opts...); err != nil {
+		return
+	}
+	ocall = &implTunnelShellCall{Call: call}
+	return
+}
+
+// TunnelForwardClientStream is the client stream for Tunnel.Forward.
+type TunnelForwardClientStream interface {
+	// RecvStream returns the receiver side of the Tunnel.Forward client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() []byte
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Tunnel.Forward client stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors
+		// encountered while sending, or if Send is called after Close or
+		// the stream has been canceled.  Blocks if there is no buffer
+		// space; will unblock when buffer space is available or after
+		// the stream has been canceled.
+		Send(item []byte) error
+		// Close indicates to the server that no more items will be sent;
+		// server Recv calls will receive io.EOF after all sent items.
+		// This is an optional call - e.g. a client might call Close if it
+		// needs to continue receiving items from the server after it's
+		// done sending.  Returns errors encountered while closing, or if
+		// Close is called after the stream has been canceled.  Like Send,
+		// blocks if there is no buffer space available.
+		Close() error
+	}
+}
+
+// TunnelForwardCall represents the call returned from Tunnel.Forward.
+type TunnelForwardCall interface {
+	TunnelForwardClientStream
+	// Finish performs the equivalent of SendStream().Close, then blocks until
+	// the server is done, and returns the positional return values for the call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implTunnelForwardCall struct {
+	ipc.Call
+	valRecv []byte
+	errRecv error
+}
+
+func (c *implTunnelForwardCall) RecvStream() interface {
+	Advance() bool
+	Value() []byte
+	Err() error
+} {
+	return implTunnelForwardCallRecv{c}
+}
+
+type implTunnelForwardCallRecv struct {
+	c *implTunnelForwardCall
+}
+
+func (c implTunnelForwardCallRecv) Advance() bool {
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implTunnelForwardCallRecv) Value() []byte {
+	return c.c.valRecv
+}
+func (c implTunnelForwardCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implTunnelForwardCall) SendStream() interface {
+	Send(item []byte) error
+	Close() error
+} {
+	return implTunnelForwardCallSend{c}
+}
+
+type implTunnelForwardCallSend struct {
+	c *implTunnelForwardCall
+}
+
+func (c implTunnelForwardCallSend) Send(item []byte) error {
+	return c.c.Send(item)
+}
+func (c implTunnelForwardCallSend) Close() error {
+	return c.c.CloseSend()
+}
+func (c *implTunnelForwardCall) Finish() (err error) {
+	err = c.Call.Finish()
+	return
+}
+
+// TunnelShellClientStream is the client stream for Tunnel.Shell.
+type TunnelShellClientStream interface {
+	// RecvStream returns the receiver side of the Tunnel.Shell client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() ServerShellPacket
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Tunnel.Shell client stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors
+		// encountered while sending, or if Send is called after Close or
+		// the stream has been canceled.  Blocks if there is no buffer
+		// space; will unblock when buffer space is available or after
+		// the stream has been canceled.
+		Send(item ClientShellPacket) error
+		// Close indicates to the server that no more items will be sent;
+		// server Recv calls will receive io.EOF after all sent items.
+		// This is an optional call - e.g. a client might call Close if it
+		// needs to continue receiving items from the server after it's
+		// done sending.  Returns errors encountered while closing, or if
+		// Close is called after the stream has been canceled.  Like Send,
+		// blocks if there is no buffer space available.
+		Close() error
+	}
+}
+
+// TunnelShellCall represents the call returned from Tunnel.Shell.
+type TunnelShellCall interface {
+	TunnelShellClientStream
+	// Finish performs the equivalent of SendStream().Close, then blocks until
+	// the server is done, and returns the positional return values for the call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() (int32, error)
+}
+
+type implTunnelShellCall struct {
+	ipc.Call
+	valRecv ServerShellPacket
+	errRecv error
+}
+
+func (c *implTunnelShellCall) RecvStream() interface {
+	Advance() bool
+	Value() ServerShellPacket
+	Err() error
+} {
+	return implTunnelShellCallRecv{c}
+}
+
+type implTunnelShellCallRecv struct {
+	c *implTunnelShellCall
+}
+
+func (c implTunnelShellCallRecv) Advance() bool {
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implTunnelShellCallRecv) Value() ServerShellPacket {
+	return c.c.valRecv
+}
+func (c implTunnelShellCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implTunnelShellCall) SendStream() interface {
+	Send(item ClientShellPacket) error
+	Close() error
+} {
+	return implTunnelShellCallSend{c}
+}
+
+type implTunnelShellCallSend struct {
+	c *implTunnelShellCall
+}
+
+func (c implTunnelShellCallSend) Send(item ClientShellPacket) error {
+	return c.c.Send(item)
+}
+func (c implTunnelShellCallSend) Close() error {
+	return c.c.CloseSend()
+}
+func (c *implTunnelShellCall) Finish() (o0 int32, err error) {
+	err = c.Call.Finish(&o0)
+	return
+}
+
+// TunnelServerMethods is the interface a server writer
+// implements for Tunnel.
+type TunnelServerMethods interface {
+	// The Forward method is used for network forwarding. All the data sent over
+	// the byte stream is forwarded to the requested network address and all the
+	// data received from that network connection is sent back in the reply
+	// stream.
+	Forward(ctx TunnelForwardContext, network string, address string) error
+	// The Shell method is used to either run shell commands remotely, or to open
+	// an interactive shell. The data received over the byte stream is sent to the
+	// shell's stdin, and the data received from the shell's stdout and stderr is
+	// sent back in the reply stream. It returns the exit status of the shell
+	// command.
+	Shell(ctx TunnelShellContext, command string, shellOpts ShellOpts) (int32, error)
+}
+
+// TunnelServerStubMethods is the server interface containing
+// Tunnel methods, as expected by ipc.Server.
+// The only difference between this interface and TunnelServerMethods
+// is the streaming methods.
+type TunnelServerStubMethods interface {
+	// The Forward method is used for network forwarding. All the data sent over
+	// the byte stream is forwarded to the requested network address and all the
+	// data received from that network connection is sent back in the reply
+	// stream.
+	Forward(ctx *TunnelForwardContextStub, network string, address string) error
+	// The Shell method is used to either run shell commands remotely, or to open
+	// an interactive shell. The data received over the byte stream is sent to the
+	// shell's stdin, and the data received from the shell's stdout and stderr is
+	// sent back in the reply stream. It returns the exit status of the shell
+	// command.
+	Shell(ctx *TunnelShellContextStub, command string, shellOpts ShellOpts) (int32, error)
+}
+
+// TunnelServerStub adds universal methods to TunnelServerStubMethods.
+type TunnelServerStub interface {
+	TunnelServerStubMethods
+	// Describe the Tunnel interfaces.
+	Describe__() []ipc.InterfaceDesc
+}
+
+// TunnelServer returns a server stub for Tunnel.
+// It converts an implementation of TunnelServerMethods into
+// an object that may be used by ipc.Server.
+func TunnelServer(impl TunnelServerMethods) TunnelServerStub {
+	stub := implTunnelServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := ipc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := ipc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implTunnelServerStub struct {
+	impl TunnelServerMethods
+	gs   *ipc.GlobState
+}
+
+func (s implTunnelServerStub) Forward(ctx *TunnelForwardContextStub, i0 string, i1 string) error {
+	return s.impl.Forward(ctx, i0, i1)
+}
+
+func (s implTunnelServerStub) Shell(ctx *TunnelShellContextStub, i0 string, i1 ShellOpts) (int32, error) {
+	return s.impl.Shell(ctx, i0, i1)
+}
+
+func (s implTunnelServerStub) Globber() *ipc.GlobState {
+	return s.gs
+}
+
+func (s implTunnelServerStub) Describe__() []ipc.InterfaceDesc {
+	return []ipc.InterfaceDesc{TunnelDesc}
+}
+
+// TunnelDesc describes the Tunnel interface.
+var TunnelDesc ipc.InterfaceDesc = descTunnel
+
+// descTunnel hides the desc to keep godoc clean.
+var descTunnel = ipc.InterfaceDesc{
+	Name:    "Tunnel",
+	PkgPath: "v.io/apps/tunnel",
+	Methods: []ipc.MethodDesc{
+		{
+			Name: "Forward",
+			Doc:  "// The Forward method is used for network forwarding. All the data sent over\n// the byte stream is forwarded to the requested network address and all the\n// data received from that network connection is sent back in the reply\n// stream.",
+			InArgs: []ipc.ArgDesc{
+				{"network", ``}, // string
+				{"address", ``}, // string
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+		},
+		{
+			Name: "Shell",
+			Doc:  "// The Shell method is used to either run shell commands remotely, or to open\n// an interactive shell. The data received over the byte stream is sent to the\n// shell's stdin, and the data received from the shell's stdout and stderr is\n// sent back in the reply stream. It returns the exit status of the shell\n// command.",
+			InArgs: []ipc.ArgDesc{
+				{"command", ``},   // string
+				{"shellOpts", ``}, // ShellOpts
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"", ``}, // int32
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+		},
+	},
+}
+
+// TunnelForwardServerStream is the server stream for Tunnel.Forward.
+type TunnelForwardServerStream interface {
+	// RecvStream returns the receiver side of the Tunnel.Forward server stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() []byte
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Tunnel.Forward server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item []byte) error
+	}
+}
+
+// TunnelForwardContext represents the context passed to Tunnel.Forward.
+type TunnelForwardContext interface {
+	ipc.ServerContext
+	TunnelForwardServerStream
+}
+
+// TunnelForwardContextStub is a wrapper that converts ipc.StreamServerCall into
+// a typesafe stub that implements TunnelForwardContext.
+type TunnelForwardContextStub struct {
+	ipc.StreamServerCall
+	valRecv []byte
+	errRecv error
+}
+
+// Init initializes TunnelForwardContextStub from ipc.StreamServerCall.
+func (s *TunnelForwardContextStub) Init(call ipc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Tunnel.Forward server stream.
+func (s *TunnelForwardContextStub) RecvStream() interface {
+	Advance() bool
+	Value() []byte
+	Err() error
+} {
+	return implTunnelForwardContextRecv{s}
+}
+
+type implTunnelForwardContextRecv struct {
+	s *TunnelForwardContextStub
+}
+
+func (s implTunnelForwardContextRecv) Advance() bool {
+	s.s.errRecv = s.s.Recv(&s.s.valRecv)
+	return s.s.errRecv == nil
+}
+func (s implTunnelForwardContextRecv) Value() []byte {
+	return s.s.valRecv
+}
+func (s implTunnelForwardContextRecv) Err() error {
+	if s.s.errRecv == io.EOF {
+		return nil
+	}
+	return s.s.errRecv
+}
+
+// SendStream returns the send side of the Tunnel.Forward server stream.
+func (s *TunnelForwardContextStub) SendStream() interface {
+	Send(item []byte) error
+} {
+	return implTunnelForwardContextSend{s}
+}
+
+type implTunnelForwardContextSend struct {
+	s *TunnelForwardContextStub
+}
+
+func (s implTunnelForwardContextSend) Send(item []byte) error {
+	return s.s.Send(item)
+}
+
+// TunnelShellServerStream is the server stream for Tunnel.Shell.
+type TunnelShellServerStream interface {
+	// RecvStream returns the receiver side of the Tunnel.Shell server stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() ClientShellPacket
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Tunnel.Shell server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item ServerShellPacket) error
+	}
+}
+
+// TunnelShellContext represents the context passed to Tunnel.Shell.
+type TunnelShellContext interface {
+	ipc.ServerContext
+	TunnelShellServerStream
+}
+
+// TunnelShellContextStub is a wrapper that converts ipc.StreamServerCall into
+// a typesafe stub that implements TunnelShellContext.
+type TunnelShellContextStub struct {
+	ipc.StreamServerCall
+	valRecv ClientShellPacket
+	errRecv error
+}
+
+// Init initializes TunnelShellContextStub from ipc.StreamServerCall.
+func (s *TunnelShellContextStub) Init(call ipc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Tunnel.Shell server stream.
+func (s *TunnelShellContextStub) RecvStream() interface {
+	Advance() bool
+	Value() ClientShellPacket
+	Err() error
+} {
+	return implTunnelShellContextRecv{s}
+}
+
+type implTunnelShellContextRecv struct {
+	s *TunnelShellContextStub
+}
+
+func (s implTunnelShellContextRecv) Advance() bool {
+	s.s.errRecv = s.s.Recv(&s.s.valRecv)
+	return s.s.errRecv == nil
+}
+func (s implTunnelShellContextRecv) Value() ClientShellPacket {
+	return s.s.valRecv
+}
+func (s implTunnelShellContextRecv) Err() error {
+	if s.s.errRecv == io.EOF {
+		return nil
+	}
+	return s.s.errRecv
+}
+
+// SendStream returns the send side of the Tunnel.Shell server stream.
+func (s *TunnelShellContextStub) SendStream() interface {
+	Send(item ServerShellPacket) error
+} {
+	return implTunnelShellContextSend{s}
+}
+
+type implTunnelShellContextSend struct {
+	s *TunnelShellContextStub
+}
+
+func (s implTunnelShellContextSend) Send(item ServerShellPacket) error {
+	return s.s.Send(item)
+}
diff --git a/tunnel/tunneld/impl.go b/tunnel/tunneld/impl.go
new file mode 100644
index 0000000..96dda4e
--- /dev/null
+++ b/tunnel/tunneld/impl.go
@@ -0,0 +1,169 @@
+package main
+
+import (
+	"github.com/kr/pty"
+
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"os"
+	"os/exec"
+	"syscall"
+
+	"v.io/apps/tunnel"
+	"v.io/apps/tunnel/tunnelutil"
+	"v.io/x/lib/vlog"
+)
+
+// T implements tunnel.TunnelServerMethods
+type T struct {
+}
+
+const nonShellErrorCode = 255
+
+func (t *T) Forward(ctx tunnel.TunnelForwardContext, network, address string) error {
+	conn, err := net.Dial(network, address)
+	if err != nil {
+		return err
+	}
+	b, _ := ctx.RemoteBlessings().ForContext(ctx)
+	name := fmt.Sprintf("RemoteBlessings:%v LocalAddr:%v RemoteAddr:%v", b, conn.LocalAddr(), conn.RemoteAddr())
+	vlog.Infof("TUNNEL START: %v", name)
+	err = tunnelutil.Forward(conn, ctx.SendStream(), ctx.RecvStream())
+	vlog.Infof("TUNNEL END  : %v (%v)", name, err)
+	return err
+}
+
+func (t *T) Shell(ctx tunnel.TunnelShellContext, command string, shellOpts tunnel.ShellOpts) (int32, error) {
+	b, _ := ctx.RemoteBlessings().ForContext(ctx)
+	vlog.Infof("SHELL START for %v: %q", b, command)
+	shell, err := findShell()
+	if err != nil {
+		return nonShellErrorCode, err
+	}
+	var c *exec.Cmd
+	// An empty command means that we need an interactive shell.
+	if len(command) == 0 {
+		c = exec.Command(shell, "-i")
+		sendMotd(ctx)
+	} else {
+		c = exec.Command(shell, "-c", command)
+	}
+
+	c.Env = []string{
+		fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
+		fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
+	}
+	c.Env = append(c.Env, shellOpts.Environment...)
+	vlog.Infof("Shell environment: %v", c.Env)
+
+	c.Dir = os.Getenv("HOME")
+	vlog.Infof("Shell CWD: %v", c.Dir)
+
+	var (
+		stdin          io.WriteCloser // We write to stdin.
+		stdout, stderr io.ReadCloser  // We read from stdout and stderr.
+		ptyFd          uintptr        // File descriptor for pty.
+	)
+
+	if shellOpts.UsePty {
+		f, err := pty.Start(c)
+		if err != nil {
+			return nonShellErrorCode, err
+		}
+		stdin = f
+		stdout = f
+		stderr = nil
+		ptyFd = f.Fd()
+
+		defer f.Close()
+
+		setWindowSize(ptyFd, shellOpts.WinSize.Rows, shellOpts.WinSize.Cols)
+	} else {
+		var err error
+		if stdin, err = c.StdinPipe(); err != nil {
+			return nonShellErrorCode, err
+		}
+		defer stdin.Close()
+
+		if stdout, err = c.StdoutPipe(); err != nil {
+			return nonShellErrorCode, err
+		}
+		defer stdout.Close()
+
+		if stderr, err = c.StderrPipe(); err != nil {
+			return nonShellErrorCode, err
+		}
+		defer stderr.Close()
+
+		if err = c.Start(); err != nil {
+			vlog.Infof("Cmd.Start failed: %v", err)
+			return nonShellErrorCode, err
+		}
+	}
+
+	defer c.Process.Kill()
+
+	select {
+	case runErr := <-runIOManager(stdin, stdout, stderr, ptyFd, ctx):
+		b, _ := ctx.RemoteBlessings().ForContext(ctx)
+		vlog.Infof("SHELL END for %v: %q (%v)", b, command, runErr)
+		return harvestExitcode(c.Process, runErr)
+	case <-ctx.Context().Done():
+		return nonShellErrorCode, fmt.Errorf("remote end exited")
+	}
+}
+
+// harvestExitcode returns the (exitcode, error) pair to be returned for the Shell RPC
+// based on the status of the process and the error returned from runIOManager
+func harvestExitcode(process *os.Process, ioerr error) (int32, error) {
+	// Check the exit status.
+	var status syscall.WaitStatus
+	if _, err := syscall.Wait4(process.Pid, &status, syscall.WNOHANG, nil); err != nil {
+		return nonShellErrorCode, err
+	}
+	if status.Signaled() {
+		return int32(status), fmt.Errorf("process killed by signal %u (%v)", status.Signal(), status.Signal())
+	}
+	if status.Exited() {
+		if status.ExitStatus() == 0 {
+			return 0, nil
+		}
+		return int32(status.ExitStatus()), fmt.Errorf("process exited with exit status %d", status.ExitStatus())
+	}
+	// The process has not exited. Use the error from ForwardStdIO.
+	return nonShellErrorCode, ioerr
+}
+
+// findShell returns the path to the first usable shell binary.
+func findShell() (string, error) {
+	shells := []string{"/bin/bash", "/bin/sh"}
+	for _, s := range shells {
+		if _, err := os.Stat(s); err == nil {
+			return s, nil
+		}
+	}
+	return "", errors.New("could not find any shell binary")
+}
+
+// sendMotd sends the content of the MOTD file to the stream, if it exists.
+func sendMotd(s tunnel.TunnelShellServerStream) {
+	data, err := ioutil.ReadFile("/etc/motd")
+	if err != nil {
+		// No MOTD. That's OK.
+		return
+	}
+	packet := tunnel.ServerShellPacketStdout{[]byte(data)}
+	if err = s.SendStream().Send(packet); err != nil {
+		vlog.Infof("Send failed: %v", err)
+	}
+}
+
+func setWindowSize(fd uintptr, row, col uint16) {
+	ws := tunnelutil.Winsize{Row: row, Col: col}
+	if err := tunnelutil.SetWindowSize(fd, ws); err != nil {
+		vlog.Infof("Failed to set window size: %v", err)
+	}
+}
diff --git a/tunnel/tunneld/iomanager.go b/tunnel/tunneld/iomanager.go
new file mode 100644
index 0000000..9f12ce2
--- /dev/null
+++ b/tunnel/tunneld/iomanager.go
@@ -0,0 +1,187 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"sync"
+
+	"v.io/apps/tunnel"
+	"v.io/x/lib/vlog"
+)
+
+func runIOManager(stdin io.WriteCloser, stdout, stderr io.Reader, ptyFd uintptr, stream tunnel.TunnelShellServerStream) <-chan error {
+	m := ioManager{stdin: stdin, stdout: stdout, stderr: stderr, ptyFd: ptyFd, stream: stream}
+	c := make(chan error, 1) // buffered channel so that the goroutine spawned below is not leaked if the channel is not read from.
+	go func() { c <- m.run() }()
+	return c
+}
+
+// ioManager manages the forwarding of all the data between the shell and the
+// stream.
+type ioManager struct {
+	stdin          io.WriteCloser
+	stdout, stderr io.Reader
+	ptyFd          uintptr
+	stream         tunnel.TunnelShellServerStream
+
+	// streamError receives errors coming from stream operations.
+	streamError chan error
+	// stdioError receives errors coming from stdio operations.
+	stdioError chan error
+}
+
+func (m *ioManager) run() error {
+	m.streamError = make(chan error, 1)
+	m.stdioError = make(chan error, 1)
+
+	var pendingShellOutput sync.WaitGroup
+	pendingShellOutput.Add(1)
+	var pendingStreamInput sync.WaitGroup
+	pendingStreamInput.Add(1)
+
+	// Forward data between the shell's stdio and the stream.
+	go func() {
+		defer pendingShellOutput.Done()
+		// outchan is used to serialize the output to the stream.
+		// chan2stream() receives data sent by stdout2outchan() and
+		// stderr2outchan() and sends it to the stream.
+		outchan := make(chan tunnel.ServerShellPacket)
+		var wgStream sync.WaitGroup
+		wgStream.Add(1)
+		go m.chan2stream(outchan, &wgStream)
+		var wgStdio sync.WaitGroup
+		wgStdio.Add(1)
+		go m.stdout2outchan(outchan, &wgStdio)
+		if m.stderr != nil {
+			wgStdio.Add(1)
+			go m.stderr2outchan(outchan, &wgStdio)
+		}
+		// When both stdout2outchan and stderr2outchan are done, close
+		// outchan to signal chan2stream to exit.
+		wgStdio.Wait()
+		close(outchan)
+		wgStream.Wait()
+	}()
+	go m.stream2stdin(&pendingStreamInput)
+
+	// Block until something reports an error.
+	//
+	// If there is any stream error, we assume that both ends of the stream
+	// have an error, e.g. if stream.Reader.Advance fails then
+	// stream.Sender.Send will fail. We process any remaining input from
+	// the stream and then return.
+	//
+	// If there is any stdio error, we assume all 3 io channels will fail
+	// (if stdout.Read fails then stdin.Write and stderr.Read will also
+	// fail). We process is remaining output from the shell and then
+	// return.
+	select {
+	case err := <-m.streamError:
+		// Process remaining input from the stream before exiting.
+		vlog.VI(2).Infof("run stream error: %v", err)
+		pendingStreamInput.Wait()
+		return err
+	case err := <-m.stdioError:
+		// Process remaining output from the shell before exiting.
+		vlog.VI(2).Infof("run stdio error: %v", err)
+		pendingShellOutput.Wait()
+		return err
+	}
+}
+
+func (m *ioManager) sendStreamError(err error) {
+	select {
+	case m.streamError <- err:
+	default:
+	}
+}
+
+func (m *ioManager) sendStdioError(err error) {
+	select {
+	case m.stdioError <- err:
+	default:
+	}
+}
+
+// chan2stream receives ServerShellPacket from outchan and sends it to stream.
+func (m *ioManager) chan2stream(outchan <-chan tunnel.ServerShellPacket, wg *sync.WaitGroup) {
+	defer wg.Done()
+	sender := m.stream.SendStream()
+	for packet := range outchan {
+		vlog.VI(3).Infof("chan2stream packet: %+v", packet)
+		if err := sender.Send(packet); err != nil {
+			vlog.VI(2).Infof("chan2stream: %v", err)
+			m.sendStreamError(err)
+		}
+	}
+}
+
+// stdout2stream reads data from the shell's stdout and sends it to the outchan.
+func (m *ioManager) stdout2outchan(outchan chan<- tunnel.ServerShellPacket, wg *sync.WaitGroup) {
+	defer wg.Done()
+	for {
+		buf := make([]byte, 2048)
+		n, err := m.stdout.Read(buf[:])
+		if err != nil {
+			vlog.VI(2).Infof("stdout2outchan: %v", err)
+			m.sendStdioError(err)
+			return
+		}
+		outchan <- tunnel.ServerShellPacketStdout{buf[:n]}
+	}
+}
+
+// stderr2stream reads data from the shell's stderr and sends it to the outchan.
+func (m *ioManager) stderr2outchan(outchan chan<- tunnel.ServerShellPacket, wg *sync.WaitGroup) {
+	defer wg.Done()
+	for {
+		buf := make([]byte, 2048)
+		n, err := m.stderr.Read(buf[:])
+		if err != nil {
+			vlog.VI(2).Infof("stderr2outchan: %v", err)
+			m.sendStdioError(err)
+			return
+		}
+		outchan <- tunnel.ServerShellPacketStderr{buf[:n]}
+	}
+}
+
+// stream2stdin reads data from the stream and sends it to the shell's stdin.
+func (m *ioManager) stream2stdin(wg *sync.WaitGroup) {
+	defer wg.Done()
+	rStream := m.stream.RecvStream()
+	for rStream.Advance() {
+		packet := rStream.Value()
+		vlog.VI(3).Infof("stream2stdin packet: %+v", packet)
+		switch v := packet.(type) {
+		case tunnel.ClientShellPacketStdin:
+			if n, err := m.stdin.Write(v.Value); n != len(v.Value) || err != nil {
+				m.sendStdioError(fmt.Errorf("stdin.Write returned (%d, %v) want (%d, nil)", n, err, len(v.Value)))
+				return
+			}
+		case tunnel.ClientShellPacketEOF:
+			if err := m.stdin.Close(); err != nil {
+				m.sendStdioError(fmt.Errorf("stdin.Close: %v", err))
+				return
+			}
+		case tunnel.ClientShellPacketWinSize:
+			size := v.Value
+			if size.Rows > 0 && size.Cols > 0 && m.ptyFd != 0 {
+				setWindowSize(m.ptyFd, size.Rows, size.Cols)
+			}
+		default:
+			vlog.Infof("unexpected message type: %T", packet)
+		}
+	}
+
+	err := rStream.Err()
+	if err == nil {
+		err = io.EOF
+	}
+
+	vlog.VI(2).Infof("stream2stdin: %v", err)
+	m.sendStreamError(err)
+	if err := m.stdin.Close(); err != nil {
+		m.sendStdioError(fmt.Errorf("stdin.Close: %v", err))
+	}
+}
diff --git a/tunnel/tunneld/main.go b/tunnel/tunneld/main.go
new file mode 100644
index 0000000..4e5b199
--- /dev/null
+++ b/tunnel/tunneld/main.go
@@ -0,0 +1,84 @@
+// Command tunneld is an implementation of the tunnel service.
+package main
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"os"
+	"strings"
+
+	"v.io/v23"
+	"v.io/x/lib/vlog"
+
+	"v.io/core/veyron/lib/signals"
+	_ "v.io/core/veyron/profiles/roaming"
+	sflag "v.io/core/veyron/security/flag"
+
+	"v.io/apps/tunnel"
+)
+
+// firstHardwareAddrInUse returns the hwaddr of the first network interface
+// that is up, excluding loopback.
+func firstHardwareAddrInUse() (string, error) {
+	interfaces, err := net.Interfaces()
+	if err != nil {
+		return "", err
+	}
+	for _, i := range interfaces {
+		if !strings.HasPrefix(i.Name, "lo") && i.Flags&net.FlagUp != 0 {
+			name := i.HardwareAddr.String()
+			if len(name) == 0 {
+				continue
+			}
+			vlog.Infof("Using %q (from %v)", name, i.Name)
+			return name, nil
+		}
+	}
+	return "", errors.New("No usable network interfaces")
+}
+
+func main() {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	auth := sflag.NewAuthorizerOrDie()
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatalf("NewServer failed: %v", err)
+	}
+	defer server.Stop()
+
+	listenSpec := v23.GetListenSpec(ctx)
+	eps, err := server.Listen(listenSpec)
+	if err != nil {
+		vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+	}
+	vlog.Infof("Listening on: %v", eps)
+	hwaddr, err := firstHardwareAddrInUse()
+	if err != nil {
+		vlog.Fatalf("Couldn't find a good hw address: %v", err)
+	}
+	hostname, err := os.Hostname()
+	if err != nil {
+		vlog.Fatalf("os.Hostname failed: %v", err)
+	}
+	names := []string{
+		fmt.Sprintf("tunnel/hostname/%s", hostname),
+		fmt.Sprintf("tunnel/hwaddr/%s", hwaddr),
+	}
+	published := false
+	if err := server.Serve(names[0], tunnel.TunnelServer(&T{}), auth); err != nil {
+		vlog.Infof("Serve(%v) failed: %v", names[0], err)
+	}
+	published = true
+	for _, n := range names[1:] {
+		server.AddName(n)
+	}
+	if !published {
+		vlog.Fatalf("Failed to publish with any of %v", names)
+	}
+	vlog.Infof("Published as %v", names)
+
+	<-signals.ShutdownOnSignals(ctx)
+}
diff --git a/tunnel/tunneld/tunneld_v23_test.go b/tunnel/tunneld/tunneld_v23_test.go
new file mode 100644
index 0000000..2b8638a
--- /dev/null
+++ b/tunnel/tunneld/tunneld_v23_test.go
@@ -0,0 +1,91 @@
+package main_test
+
+//go:generate v23 test generate .
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"v.io/core/veyron/lib/testutil"
+	"v.io/core/veyron/lib/testutil/v23tests"
+)
+
+func V23TestTunneld(t *v23tests.T) {
+	v23tests.RunRootMT(t, "--veyron.tcp.address=127.0.0.1:0")
+
+	tunneldBin := t.BuildGoPkg("v.io/apps/tunnel/tunneld")
+	vsh := t.BuildGoPkg("v.io/apps/tunnel/vsh")
+	mounttableBin := t.BuildGoPkg("v.io/core/veyron/tools/mounttable")
+
+	port, err := testutil.FindUnusedPort()
+	if err != nil {
+		t.Fatalf("FindUnusedPort failed: %v", err)
+	}
+
+	tunnelAddress := fmt.Sprintf("127.0.0.1:%d", port)
+	tunnelEndpoint := "/" + tunnelAddress
+
+	// Start tunneld with a known endpoint.
+	tunneldBin.Start("--veyron.tcp.address=" + tunnelAddress)
+
+	// Run remote command with the endpoint.
+	if want, got := "HELLO ENDPOINT\n", vsh.Start(tunnelEndpoint, "echo", "HELLO", "ENDPOINT").Output(); want != got {
+		t.Fatalf("unexpected output, got %s, want %s", got, want)
+	}
+
+	// Run remote command with the object name.
+	hostname, err := os.Hostname()
+	if err != nil {
+		t.Fatalf("Hostname() failed: %v", err)
+	}
+
+	if want, got := "HELLO NAME\n", vsh.Start("tunnel/hostname/"+hostname, "echo", "HELLO", "NAME").Output(); want != got {
+		t.Fatalf("unexpected output, got %s, want %s", got, want)
+	}
+
+	// Send input to remote command.
+	want := "HELLO SERVER"
+	if got := vsh.WithStdin(bytes.NewBufferString(want)).Start(tunnelEndpoint, "cat").Output(); want != got {
+		t.Fatalf("unexpected output, got %s, want %s", got, want)
+	}
+
+	// And again with a file redirection this time.
+	outDir := t.NewTempDir()
+	outPath := filepath.Join(outDir, "hello.txt")
+
+	// TODO(sjr): instead of using Output() here, we'd really rather do
+	// WaitOrDie(os.Stdout, os.Stderr). There is currently a race caused by
+	// WithStdin that makes this flaky.
+	vsh.WithStdin(bytes.NewBufferString(want)).Start(tunnelEndpoint, "cat > "+outPath).Output()
+	if got, err := ioutil.ReadFile(outPath); err != nil || string(got) != want {
+		if err != nil {
+			t.Fatalf("ReadFile(%v) failed: %v", outPath, err)
+		} else {
+			t.Fatalf("unexpected output, got %s, want %s", got, want)
+		}
+	}
+
+	// Verify that all published names are there.
+	root, _ := t.GetVar("NAMESPACE_ROOT")
+	inv := mounttableBin.Start("glob", root, "tunnel/*/*")
+
+	// Expect two entries: one for the tunnel hostname and one for its hwaddr.
+	matches := inv.ExpectSetEventuallyRE(
+		"tunnel/hostname/"+regexp.QuoteMeta(hostname)+" (.*) \\(TTL .*\\)",
+		"tunnel/hwaddr/.* (.*) \\(TTL .*\\)")
+
+	// The full endpoint should contain the address we initially specified for the tunnel.
+	if want = "@" + tunnelAddress + "@"; !strings.Contains(matches[0][1], want) {
+		t.Fatalf("expected tunnel endpoint %s to contain %s, but it did not", matches[0][1], want)
+	}
+
+	// The hwaddr endpoint should be the same as the hostname endpoint.
+	if matches[0][1] != matches[1][1] {
+		t.Fatalf("expected hwaddr and hostname tunnel endpoints to match, but they did not (%s != %s)", matches[0][1], matches[1][1])
+	}
+}
diff --git a/tunnel/tunneld/v23_test.go b/tunnel/tunneld/v23_test.go
new file mode 100644
index 0000000..d45c99d
--- /dev/null
+++ b/tunnel/tunneld/v23_test.go
@@ -0,0 +1,25 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package main_test
+
+import "testing"
+import "os"
+
+import "v.io/core/veyron/lib/testutil"
+import "v.io/core/veyron/lib/testutil/v23tests"
+
+func TestMain(m *testing.M) {
+	testutil.Init()
+	cleanup := v23tests.UseSharedBinDir()
+	r := m.Run()
+	cleanup()
+	os.Exit(r)
+}
+
+func TestV23Tunneld(t *testing.T) {
+	v23tests.RunTest(t, V23TestTunneld)
+}
diff --git a/tunnel/tunnelutil/forward.go b/tunnel/tunnelutil/forward.go
new file mode 100644
index 0000000..bc046d3
--- /dev/null
+++ b/tunnel/tunnelutil/forward.go
@@ -0,0 +1,64 @@
+package tunnelutil
+
+import (
+	"fmt"
+	"io"
+	"net"
+)
+
+type sender interface {
+	Send([]uint8) error
+}
+type receiver interface {
+	Advance() bool
+
+	Value() []uint8
+
+	Err() error
+}
+
+// Forward forwards data read from net.Conn to a TunnelForwardClientStream or a
+// TunnelForwardServerStream.
+func Forward(conn net.Conn, s sender, r receiver) error {
+	defer conn.Close()
+	// Both conn2stream and stream2conn will write to the channel exactly
+	// once.
+	// Forward reads from the channel exactly once.
+	// A buffered channel is used to prevent the other write to the channel
+	// from blocking.
+	done := make(chan error, 1)
+	go conn2stream(conn, s, done)
+	go stream2conn(r, conn, done)
+	return <-done
+}
+
+func conn2stream(r io.Reader, s sender, done chan error) {
+	var buf [2048]byte
+	for {
+		n, err := r.Read(buf[:])
+		if err == io.EOF {
+			done <- nil
+			return
+		}
+		if err != nil {
+			done <- err
+			return
+		}
+		if err := s.Send(buf[:n]); err != nil {
+			done <- err
+			return
+		}
+	}
+}
+
+func stream2conn(r receiver, w io.Writer, done chan error) {
+	for r.Advance() {
+		buf := r.Value()
+
+		if n, err := w.Write(buf); n != len(buf) || err != nil {
+			done <- fmt.Errorf("conn.Write returned (%d, %v) want (%d, nil)", n, err, len(buf))
+			return
+		}
+	}
+	done <- r.Err()
+}
diff --git a/tunnel/tunnelutil/terminal.go b/tunnel/tunnelutil/terminal.go
new file mode 100644
index 0000000..92f178f
--- /dev/null
+++ b/tunnel/tunnelutil/terminal.go
@@ -0,0 +1,95 @@
+// Package tunnelutil contains a set of common types and functions
+// used by both tunnel service clients and servers.
+package tunnelutil
+
+import (
+	"errors"
+	"os/exec"
+	"strings"
+	"syscall"
+	"unsafe"
+
+	"v.io/x/lib/vlog"
+)
+
+// Used with ioctl TIOCGWINSZ and TIOCSWINSZ.
+type Winsize struct {
+	Row    uint16
+	Col    uint16
+	Xpixel uint16
+	Ypixel uint16
+}
+
+// SetWindowSize sets the terminal's window size.
+func SetWindowSize(fd uintptr, ws Winsize) error {
+	vlog.Infof("Setting window size: %v", ws)
+	ret, _, _ := syscall.Syscall(
+		syscall.SYS_IOCTL,
+		fd,
+		uintptr(syscall.TIOCSWINSZ),
+		uintptr(unsafe.Pointer(&ws)))
+	if int(ret) == -1 {
+		return errors.New("ioctl(TIOCSWINSZ) failed")
+	}
+	return nil
+}
+
+// GetWindowSize gets the terminal's window size.
+func GetWindowSize() (*Winsize, error) {
+	ws := &Winsize{}
+	ret, _, _ := syscall.Syscall(
+		syscall.SYS_IOCTL,
+		uintptr(syscall.Stdin),
+		uintptr(syscall.TIOCGWINSZ),
+		uintptr(unsafe.Pointer(ws)))
+	if int(ret) == -1 {
+		return nil, errors.New("ioctl(TIOCGWINSZ) failed")
+	}
+	return ws, nil
+}
+
+func EnterRawTerminalMode() string {
+	var savedBytes []byte
+	var err error
+	if savedBytes, err = exec.Command("stty", "-F", "/dev/tty", "-g").Output(); err != nil {
+		vlog.Infof("Failed to save terminal settings: %q (%v)", savedBytes, err)
+	}
+	saved := strings.TrimSpace(string(savedBytes))
+
+	args := []string{
+		"-F", "/dev/tty",
+		// Don't buffer stdin. Read characters as they are typed.
+		"-icanon", "min", "1", "time", "0",
+		// Turn off local echo of input characters.
+		"-echo", "-echoe", "-echok", "-echonl",
+		// Disable interrupt, quit, and suspend special characters.
+		"-isig",
+		// Ignore characters with parity errors.
+		"ignpar",
+		// Disable translate newline to carriage return.
+		"-inlcr",
+		// Disable ignore carriage return.
+		"-igncr",
+		// Disable translate carriage return to newline.
+		"-icrnl",
+		// Disable flow control.
+		"-ixon", "-ixany", "-ixoff",
+		// Disable non-POSIX special characters.
+		"-iexten",
+	}
+	if out, err := exec.Command("stty", args...).CombinedOutput(); err != nil {
+		vlog.Infof("stty failed (%v) (%q)", err, out)
+	}
+
+	return string(saved)
+}
+
+func RestoreTerminalSettings(saved string) {
+	args := []string{
+		"-F", "/dev/tty",
+		saved,
+	}
+	if out, err := exec.Command("stty", args...).CombinedOutput(); err != nil {
+		vlog.Infof("stty failed (%v) (%q)", err, out)
+	}
+}
diff --git a/tunnel/vsh/iomanager.go b/tunnel/vsh/iomanager.go
new file mode 100644
index 0000000..9effbd9
--- /dev/null
+++ b/tunnel/vsh/iomanager.go
@@ -0,0 +1,182 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+
+	"v.io/apps/tunnel"
+	"v.io/apps/tunnel/tunnelutil"
+	"v.io/x/lib/vlog"
+)
+
+func runIOManager(stdin io.Reader, stdout, stderr io.Writer, stream tunnel.TunnelShellCall) error {
+	m := ioManager{stdin: stdin, stdout: stdout, stderr: stderr, stream: stream}
+	return m.run()
+}
+
+// ioManager manages the forwarding of all the data between the shell and the
+// stream.
+type ioManager struct {
+	stdin          io.Reader
+	stdout, stderr io.Writer
+	stream         tunnel.TunnelShellCall
+
+	// streamError receives errors coming from stream operations.
+	streamError chan error
+	// stdioError receives errors coming from stdio operations.
+	stdioError chan error
+}
+
+func (m *ioManager) run() error {
+	m.streamError = make(chan error, 1)
+	m.stdioError = make(chan error, 1)
+
+	var pendingUserInput sync.WaitGroup
+	pendingUserInput.Add(1)
+	var pendingStreamOutput sync.WaitGroup
+	pendingStreamOutput.Add(1)
+
+	// Forward data between the user and the remote shell.
+	go func() {
+		defer pendingUserInput.Done()
+		// outchan is used to serialize the output to the stream.
+		// chan2stream() receives data sent by handleWindowResize() and
+		// user2outchan() and sends it to the stream.
+		outchan := make(chan tunnel.ClientShellPacket)
+		var wgStream sync.WaitGroup
+		wgStream.Add(1)
+		go m.chan2stream(outchan, &wgStream)
+
+		// When the terminal window is resized, we receive a SIGWINCH. Then we
+		// send the new window size to the server.
+		winch := make(chan os.Signal, 1)
+		signal.Notify(winch, syscall.SIGWINCH)
+
+		var wgUser sync.WaitGroup
+		wgUser.Add(2)
+		go func() {
+			m.user2outchan(outchan, &wgUser)
+			signal.Stop(winch)
+			close(winch)
+		}()
+		go m.handleWindowResize(winch, outchan, &wgUser)
+		// When both user2outchan and handleWindowResize are done,
+		// close outchan to signal chan2stream to exit.
+		wgUser.Wait()
+		close(outchan)
+		wgStream.Wait()
+	}()
+	go m.stream2user(&pendingStreamOutput)
+	// Block until something reports an error.
+	select {
+	case err := <-m.streamError:
+		// When we receive an error from the stream, wait for any
+		// remaining stream output to be sent to the user before
+		// exiting.
+		vlog.VI(2).Infof("run stream error: %v", err)
+		pendingStreamOutput.Wait()
+		return err
+	case err := <-m.stdioError:
+		// When we receive an error from the user, wait for any
+		// remaining input from the user to be sent to the stream
+		// before exiting.
+		vlog.VI(2).Infof("run stdio error: %v", err)
+		pendingUserInput.Wait()
+		return err
+	}
+}
+
+func (m *ioManager) sendStreamError(err error) {
+	select {
+	case m.streamError <- err:
+	default:
+	}
+}
+
+func (m *ioManager) sendStdioError(err error) {
+	select {
+	case m.stdioError <- err:
+	default:
+	}
+}
+
+// chan2stream receives ClientShellPacket from outchan and sends it to stream.
+func (m *ioManager) chan2stream(outchan <-chan tunnel.ClientShellPacket, wg *sync.WaitGroup) {
+	defer wg.Done()
+	sender := m.stream.SendStream()
+	for packet := range outchan {
+		vlog.VI(3).Infof("chan2stream packet: %+v", packet)
+		if err := sender.Send(packet); err != nil {
+			vlog.VI(2).Infof("chan2stream: %v", err)
+			m.sendStreamError(err)
+		}
+	}
+	m.sendStreamError(io.EOF)
+}
+
+func (m *ioManager) handleWindowResize(winch <-chan os.Signal, outchan chan<- tunnel.ClientShellPacket, wg *sync.WaitGroup) {
+	defer wg.Done()
+	for _ = range winch {
+		ws, err := tunnelutil.GetWindowSize()
+		if err != nil {
+			vlog.Infof("GetWindowSize failed: %v", err)
+			continue
+		}
+		outchan <- tunnel.ClientShellPacketWinSize{tunnel.WindowSize{ws.Row, ws.Col}}
+	}
+}
+
+// user2stream reads input from stdin and sends it to the outchan.
+func (m *ioManager) user2outchan(outchan chan<- tunnel.ClientShellPacket, wg *sync.WaitGroup) {
+	defer wg.Done()
+	for {
+		buf := make([]byte, 2048)
+		n, err := m.stdin.Read(buf[:])
+		if err == io.EOF {
+			vlog.VI(2).Infof("user2outchan: EOF, closing stdin")
+			outchan <- tunnel.ClientShellPacketEOF{}
+			return
+		}
+		if err != nil {
+			vlog.VI(2).Infof("user2outchan: %v", err)
+			m.sendStdioError(err)
+			return
+		}
+		outchan <- tunnel.ClientShellPacketStdin{buf[:n]}
+	}
+}
+
+// stream2user reads data from the stream and sends it to either stdout or stderr.
+func (m *ioManager) stream2user(wg *sync.WaitGroup) {
+	defer wg.Done()
+	rStream := m.stream.RecvStream()
+	for rStream.Advance() {
+		packet := rStream.Value()
+		vlog.VI(3).Infof("stream2user packet: %+v", packet)
+
+		switch v := packet.(type) {
+		case tunnel.ServerShellPacketStdout:
+			if n, err := m.stdout.Write(v.Value); n != len(v.Value) || err != nil {
+				m.sendStdioError(fmt.Errorf("stdout.Write returned (%d, %v) want (%d, nil)", n, err, len(v.Value)))
+				return
+			}
+		case tunnel.ServerShellPacketStderr:
+			if n, err := m.stderr.Write(v.Value); n != len(v.Value) || err != nil {
+				m.sendStdioError(fmt.Errorf("stderr.Write returned (%d, %v) want (%d, nil)", n, err, len(v.Value)))
+				return
+			}
+		default:
+			vlog.Infof("unexpected message type: %T", packet)
+		}
+	}
+	err := rStream.Err()
+	if err == nil {
+		err = io.EOF
+	}
+	vlog.VI(2).Infof("stream2user: %v", err)
+	m.sendStreamError(err)
+}
diff --git a/tunnel/vsh/main.go b/tunnel/vsh/main.go
new file mode 100644
index 0000000..a66ea11
--- /dev/null
+++ b/tunnel/vsh/main.go
@@ -0,0 +1,210 @@
+// Command vsh is a tunnel service client that can be used to start a
+// shell on the server.
+package main
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"net"
+	"os"
+	"path"
+	"strings"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/x/lib/vlog"
+
+	_ "v.io/core/veyron/profiles"
+
+	"v.io/apps/tunnel"
+	"v.io/apps/tunnel/tunnelutil"
+	"v.io/core/veyron/lib/signals"
+)
+
+var (
+	disablePty = flag.Bool("T", false, "Disable pseudo-terminal allocation.")
+	forcePty   = flag.Bool("t", false, "Force allocation of pseudo-terminal.")
+
+	portforward = flag.String("L", "", "localaddr,remoteaddr Forward local 'localaddr' to 'remoteaddr'")
+	lprotocol   = flag.String("local_protocol", "tcp", "Local network protocol for port forwarding")
+	rprotocol   = flag.String("remote_protocol", "tcp", "Remote network protocol for port forwarding")
+
+	noshell = flag.Bool("N", false, "Do not execute a shell. Only do port forwarding.")
+)
+
+func init() {
+	flag.Usage = func() {
+		bname := path.Base(os.Args[0])
+		fmt.Fprintf(os.Stderr, `%s: Veyron SHell.
+
+This tool is used to run shell commands or an interactive shell on a remote
+tunneld service.
+
+To open an interactive shell, use:
+  %s <object name>
+
+To run a shell command, use:
+  %s <object name> <command to run>
+
+The -L flag will forward connections from a local port to a remote address
+through the tunneld service. The flag value is localaddr,remoteaddr. E.g.
+  -L :14141,www.google.com:80
+
+%s can't be used directly with tools like rsync because veyron object names
+don't look like traditional hostnames, which rsync doesn't understand. For
+compatibility with such tools, %s has a special feature that allows passing the
+veyron object name via the VSH_NAME environment variable.
+
+  $ VSH_NAME=<object name> rsync -avh -e %s /foo/* veyron:/foo/
+
+In this example, the "veyron" host will be substituted with $VSH_NAME by %s
+and rsync will work as expected.
+
+Full flags:
+`, os.Args[0], bname, bname, bname, bname, os.Args[0], bname)
+		flag.PrintDefaults()
+	}
+}
+
+func main() {
+	// Work around the fact that os.Exit doesn't run deferred functions.
+	os.Exit(realMain())
+}
+
+func realMain() int {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	oname, cmd, err := objectNameAndCommandLine()
+	if err != nil {
+		flag.Usage()
+		fmt.Fprintf(os.Stderr, "\n%v\n", err)
+		return 1
+	}
+
+	t := tunnel.TunnelClient(oname)
+
+	if len(*portforward) > 0 {
+		go runPortForwarding(ctx, t, oname)
+	}
+
+	if *noshell {
+		<-signals.ShutdownOnSignals(ctx)
+		return 0
+	}
+
+	opts := shellOptions(cmd)
+
+	stream, err := t.Shell(ctx, cmd, opts)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+		return 1
+	}
+	if opts.UsePty {
+		saved := tunnelutil.EnterRawTerminalMode()
+		defer tunnelutil.RestoreTerminalSettings(saved)
+	}
+	runIOManager(os.Stdin, os.Stdout, os.Stderr, stream)
+
+	exitMsg := fmt.Sprintf("Connection to %s closed.", oname)
+	exitStatus, err := stream.Finish()
+	if err != nil {
+		exitMsg += fmt.Sprintf(" (%v)", err)
+	}
+	vlog.VI(1).Info(exitMsg)
+	// Only show the exit message on stdout for interactive shells.
+	// Otherwise, the exit message might get confused with the output
+	// of the command that was run.
+	if err != nil {
+		fmt.Fprintln(os.Stderr, exitMsg)
+	} else if len(cmd) == 0 {
+		fmt.Println(exitMsg)
+	}
+	return int(exitStatus)
+}
+
+func shellOptions(cmd string) (opts tunnel.ShellOpts) {
+	opts.UsePty = (len(cmd) == 0 || *forcePty) && !*disablePty
+	opts.Environment = environment()
+	ws, err := tunnelutil.GetWindowSize()
+	if err != nil {
+		vlog.VI(1).Infof("GetWindowSize failed: %v", err)
+	} else {
+		opts.WinSize.Rows = ws.Row
+		opts.WinSize.Cols = ws.Col
+	}
+	return
+}
+
+func environment() []string {
+	env := []string{}
+	for _, name := range []string{"TERM", "COLORTERM"} {
+		if value := os.Getenv(name); value != "" {
+			env = append(env, fmt.Sprintf("%s=%s", name, value))
+		}
+	}
+	return env
+}
+
+// objectNameAndCommandLine extracts the object name and the remote command to
+// send to the server. The object name is the first non-flag argument.
+// The command line is the concatenation of all non-flag arguments excluding
+// the object name.
+func objectNameAndCommandLine() (string, string, error) {
+	args := flag.Args()
+	if len(args) < 1 {
+		return "", "", errors.New("object name missing")
+	}
+	name := args[0]
+	args = args[1:]
+	// For compatibility with tools like rsync. Because object names
+	// don't look like traditional hostnames, tools that work with rsh and
+	// ssh can't work directly with vsh. This trick makes the following
+	// possible:
+	//   $ VSH_NAME=<object name> rsync -avh -e vsh /foo/* veyron:/foo/
+	// The "veyron" host will be substituted with <object name>.
+	if envName := os.Getenv("VSH_NAME"); len(envName) > 0 && name == "veyron" {
+		name = envName
+	}
+	cmd := strings.Join(args, " ")
+	return name, cmd, nil
+}
+
+func runPortForwarding(ctx *context.T, t tunnel.TunnelClientMethods, oname string) {
+	// *portforward is localaddr,remoteaddr
+	parts := strings.Split(*portforward, ",")
+	var laddr, raddr string
+	if len(parts) != 2 {
+		vlog.Fatalf("-L flag expects 2 values separated by a comma")
+	}
+	laddr = parts[0]
+	raddr = parts[1]
+
+	ln, err := net.Listen(*lprotocol, laddr)
+	if err != nil {
+		vlog.Fatalf("net.Listen(%q, %q) failed: %v", *lprotocol, laddr, err)
+	}
+	defer ln.Close()
+	vlog.VI(1).Infof("Listening on %q", ln.Addr())
+	for {
+		conn, err := ln.Accept()
+		if err != nil {
+			vlog.Infof("Accept failed: %v", err)
+			continue
+		}
+		stream, err := t.Forward(ctx, *rprotocol, raddr)
+		if err != nil {
+			vlog.Infof("Tunnel(%q, %q) failed: %v", *rprotocol, raddr, err)
+			conn.Close()
+			continue
+		}
+		name := fmt.Sprintf("%v-->%v-->(%v)-->%v", conn.RemoteAddr(), conn.LocalAddr(), oname, raddr)
+		go func() {
+			vlog.VI(1).Infof("TUNNEL START: %v", name)
+			errf := tunnelutil.Forward(conn, stream.SendStream(), stream.RecvStream())
+			err := stream.Finish()
+			vlog.VI(1).Infof("TUNNEL END  : %v (%v, %v)", name, errf, err)
+		}()
+	}
+}