This post originated from an RSS feed registered with Java Buzz
by Brian McCallister.
Original Post: RPC Over SSH and Domain Sockets
Feed Title: Waste of Time
Feed URL: http://kasparov.skife.org/blog/index.rss
Feed Description: A simple waste of time and weblog experiment
I really like using SSH for authentication and authorization when possible – it is very configurable, well understood, and more secure then anything I am likely to design. It is also generally pretty easy to have applications communicate over SSH. A nice model is to have the server listen on a domain socket in a directory with appropriate permissions, and clients connect over ssh and netcat to talk to it.
Logically, on the client it is:
$ ssh server.example.com /usr/bin/nc -U /tmp/foo
And voila, your client (or shell in this case) is connected to the remote domain socket. After finding Jeff Hodges’s wonderful writeup on go.crypto/ssh I sat down to make Go do this internally. It was fun, and pretty straightforward.
The server is just a net/rpc server which listens on a domain socket and responds with a greeting:
packagemainimport("fmt""log""net""net/rpc""os""os/signal""syscall")// rpc responsetypeResponsestruct{Greetingstring}// rpc requesttypeRequeststruct{Namestring}// rpc host struct thingtypeGreeterstruct{}// our remotely invocable functionfunc(g*Greeter)Greet(reqRequest,res*Response)(errerror){res.Greeting=fmt.Sprintf("Hello %s",req.Name)return}// start up rpc listener at pathfuncServeAt(pathstring)(errerror){rpc.Register(&Greeter{})listener,err:=net.Listen("unix",path)iferr!=nil{returnfmt.Errorf("unable to listen at %s: %s",path,err)}gorpc.Accept(listener)return}// ./server /tmp/foofuncmain(){path:=os.Args[1]err:=ServeAt(path)iferr!=nil{log.Fatalf("failed: %s",err)}deferos.Remove(path)// block until we are signalled to quitwait()}funcwait(){signals:=make(chanos.Signal)signal.Notify(signals,syscall.SIGINT,syscall.SIGKILL,syscall.SIGHUP)<-signals}
The client is the fun part. It establishes an SSH connection to the server host, then fires off a Session against netcat, attaches an RPC client to that session, and does its stuff!
packagemainimport("code.google.com/p/go.crypto/ssh""fmt""io""log""net""net/rpc""os""strings")// RPC response containertypeResponsestruct{Greetingstring}// RPC request containertypeRequeststruct{Namestring}// It would be nice if ssh.Session was an io.ReaderWriter// proposal submitted :-)typeNetCatSessionstruct{*ssh.Session// define Close()writerio.Writerreaderio.Reader}// io.Readerfunc(sNetCatSession)Read(p[]byte)(nint,errerror){returns.reader.Read(p)}// io.Writerfunc(sNetCatSession)Write(p[]byte)(nint,errerror){returns.writer.Write(p)}// given the established ssh connection, start a session against netcat and// return a io.ReaderWriterCloser appropriate for rpc.NewClient(...)funcStartNetCat(client*ssh.ClientConn,pathstring)(rwc*NetCatSession,errerror){session,err:=client.NewSession()iferr!=nil{return}cmd:=fmt.Sprintf("/usr/bin/nc -U %s",path)in,err:=session.StdinPipe()iferr!=nil{returnnil,fmt.Errorf("unable to get stdin: %s",err)}out,err:=session.StdoutPipe()iferr!=nil{returnnil,fmt.Errorf("unable to get stdout: %s",err)}err=session.Start(cmd)iferr!=nil{returnnil,fmt.Errorf("unable to start '%s': %s",cmd,err)}return&NetCatSession{session,in,out},nil}// ./client localhost:/tmp/foo Brianfuncmain(){parts:=strings.Split(os.Args[1],":")host:=parts[0]path:=parts[1]name:=os.Args[2]// SSH setup, we assume current username and use the ssh agent// for authagent_sock,err:=net.Dial("unix",os.Getenv("SSH_AUTH_SOCK"))iferr!=nil{log.Fatalf("sorry, this example requires the ssh agent: %s",err)}deferagent_sock.Close()config:=&ssh.ClientConfig{User:os.Getenv("USER"),Auth:[]ssh.ClientAuth{ssh.ClientAuthAgent(ssh.NewAgentClient(agent_sock)),},}ssh_client,err:=ssh.Dial("tcp",fmt.Sprintf("%s:22",host),config)iferr!=nil{log.Fatalf("Failed to dial: %s",err)}deferssh_client.Close()// Establish sesstion to netcat talking to the domain sockets,err:=StartNetCat(ssh_client,path)iferr!=nil{log.Fatalf("unable to start netcat session: %s",err)}// now comes the RPC!client:=rpc.NewClient(s)deferclient.Close()req:=&Request{name}varresResponseerr=client.Call("Greeter.Greet",req,&res)iferr!=nil{log.Fatalf("error in rpc: %s",err)}fmt.Println(res.Greeting)}
And there it is! This isn’t exactly library code, but it nicely bundles up how to do it.
I really like using domain sockets and SSH for “operational” stuff. The slight overhead of firing up extra processes on the server, and hopping between tcp and unix sockets doesn’t usually matter, and you get lots of nice well understood and configurable security for your sessions.
In this case, I’m using SSH-as-a-library, in the past I have shelled out to SSH in order to take advantage of client side SSH configuration as well. Which makes the most sense varies, of course :-)