Add ephemeral onion service functionality to controller API

here we also:
- add a TorDialer and OnionListener
- complete working example of an onion echo server
This commit is contained in:
David Stainton 2015-09-30 18:02:47 +02:00
parent 8f9a7ec076
commit 2beeb7bc51
3 changed files with 344 additions and 0 deletions

62
cmd_onion.go Normal file
View File

@ -0,0 +1,62 @@
// cmd_onion.go - various onion service commands: ADD_ONION, DEL_ONION...
//
// To the extent possible under law, David Stainton waived all copyright
// and related or neighboring rights to this module of bulb, using the creative
// commons "cc0" public domain dedication. See LICENSE or
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
package bulb
import (
"fmt"
"strings"
)
// OnionInfo is the result of the AddOnion command.
type OnionInfo struct {
OnionId string
KeyType string
Key string
RawResponse *Response
}
// AddOnion issues an ADD_ONION command and returns the parsed response.
func (c *Conn) AddOnion(virtPort int, target, keyType, keyContent string, new bool) (*OnionInfo, error) {
var fields []string
request := "ADD_ONION "
onionInfo := OnionInfo{}
if new {
request += "NEW:BEST"
} else {
request += fmt.Sprintf("%s:%s", keyType, keyContent)
}
request += fmt.Sprintf(" Port=%d,%s\n", virtPort, target)
fmt.Printf("DEBUG request: %s\n", request)
response, err := c.Request(request)
if err != nil {
return nil, err
}
onionInfo.RawResponse = response
fields = strings.Split(fmt.Sprintf("%s", response.Data), "ServiceID=")
fields = strings.Split(fields[1], " ")
onionInfo.OnionId = fields[0]
if new {
fields = strings.Split(fmt.Sprintf("%s", response.Data), "PrivateKey=")
fields = strings.Split(fields[1], ":")
onionInfo.KeyType = fields[0]
fields = strings.Split(fields[1], "\n")
onionInfo.Key = fields[0]
}
return &onionInfo, nil
}
func (c *Conn) DeleteOnion(serviceId string) error {
var deleteCmd string = fmt.Sprintf("DEL_ONION %s\n", serviceId)
_, err := c.Request(deleteCmd)
return err
}

76
examples/onion/onion.go Normal file
View File

@ -0,0 +1,76 @@
// Onion example.
//
// To the extent possible under law, David Stainton waived all copyright
// and related or neighboring rights to this bulb source file, using the creative
// commons "cc0" public domain dedication. See LICENSE or
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
package main
import (
"fmt"
"io"
"log"
"net"
"github.com/yawning/bulb"
"github.com/yawning/bulb/utils"
)
func main() {
// Connect to a running tor instance.
// unix domain socket - c, err := bulb.Dial("unix", "/var/run/tor/control")
c, err := bulb.Dial("tcp4", "127.0.0.1:9051")
if err != nil {
log.Fatalf("failed to connect to control port: %v", err)
}
defer c.Close()
// See what's really going on under the hood.
// Do not enable in production.
c.Debug(true)
// Authenticate with the control port. The password argument
// here can be "" if no password is set (CookieAuth, no auth).
if err := c.Authenticate("ExamplePassword"); err != nil {
log.Fatalf("Authentication failed: %v", err)
}
options := utils.OnionListenerOptions{
OnionKeyFile: "echoOnionKey",
OnionServicePort: 80,
LocalAddr: "127.0.0.1:8080",
ControlNetwork: "tcp",
ControlAddr: "127.0.0.1:9051",
}
listener, err := utils.NewOnionListener(&options)
if err != nil {
log.Fatal(err)
}
addr := listener.Addr()
onionAddr := addr.String()
fmt.Printf("onion echo server: listening to %s\n", onionAddr)
defer listener.Close()
for {
// Wait for a connection.
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
// Handle the connection in a new goroutine.
// The loop then returns to accepting, so that
// multiple connections may be served concurrently.
go func(c net.Conn) {
// Echo all incoming data.
io.Copy(c, c)
// Shut down the connection.
c.Close()
}(conn)
}
}

206
utils/connection.go Normal file
View File

@ -0,0 +1,206 @@
// connection.go - A TorDialer and OnionListener...
//
// To the extent possible under law, David Stainton waived all copyright
// and related or neighboring rights to this bulb source file, using the creative
// commons "cc0" public domain dedication. See LICENSE or
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
// Package utils implements useful utilities for dealing with Tor and it's
// control port.
package utils
import (
"fmt"
"golang.org/x/net/proxy"
"io/ioutil"
"net"
"strings"
"github.com/yawning/bulb"
)
type TorDialer struct {
dialer proxy.Dialer
}
func NewTorDialer(network string, addr string, auth *proxy.Auth) (*TorDialer, error) {
var err error
var socksDialer proxy.Dialer
forwardDialer := net.Dialer{}
socksDialer, err = proxy.SOCKS5(network, addr, auth, &forwardDialer)
if err != nil {
return nil, fmt.Errorf("TorDialer: Failed to create socks dialer: %v\n", err)
}
torDialer := TorDialer{
dialer: socksDialer,
}
return &torDialer, nil
}
func (t *TorDialer) Dial(network, addr string) (net.Conn, error) {
conn, err := t.dialer.Dial(network, addr)
if err != nil {
return nil, fmt.Errorf("TorDialer: failed to dial tor socks port: %s\n", err)
}
return conn, nil
}
type OnionAddr struct {
network string
address string
}
func (o OnionAddr) Network() string {
return o.network
}
func (o OnionAddr) String() string {
return o.address
}
type OnionListenerOptions struct {
// OnionKeyFile is used to persist onion service key material
OnionKeyFile string
// OnionServicePort is the virtport of the tor onion service
OnionServicePort int
// LocalAddr is the address of the local TCP Listener
LocalAddr string
// ControlAddr is the Tor control port address
ControlAddr string
// ControlNetwork is the network type of the Tor control port
ControlNetwork string
ControlPassword string
}
type OnionListener struct {
options *OnionListenerOptions
controller *bulb.Conn
onionInfo *bulb.OnionInfo
localListener net.Listener
}
func NewOnionListener(options *OnionListenerOptions) (*OnionListener, error) {
listener := OnionListener{
options: options,
}
if options.ControlAddr == "" || options.ControlNetwork == "" {
return nil, fmt.Errorf("Tor control port address not specified.")
}
err := listener.initOnion()
if err != nil {
return nil, err
}
return &listener, nil
}
func (o *OnionListener) controlAuth() error {
var err error
o.controller, err = bulb.Dial(o.options.ControlNetwork, o.options.ControlAddr)
if err != nil {
return fmt.Errorf("OnionListener: Failed to connect to Tor control port: %s\n", err)
}
err = o.controller.Authenticate(o.options.ControlPassword)
if err != nil {
return fmt.Errorf("OnionListener: Failed authenticate with Tor control port: %s\n", err)
}
return nil
}
// readOnionKeyFile returns the key type string, key content string and nil error
// ...otherwise error will be non-nil.
func (o *OnionListener) readOnionKeyFile() (string, string, error) {
var fields []string
onionKeyBytes := make([]byte, 100)
onionKeyBytes, err := ioutil.ReadFile(o.options.OnionKeyFile)
if err != nil {
return "", "", err
}
fields = strings.Split(string(onionKeyBytes), ":")
return fields[0], fields[1], nil
}
func (o *OnionListener) writeOnionKeyFile(keyType, keyContent string) error {
keyData := fmt.Sprintf("%s:%s", keyType, keyContent)
err := ioutil.WriteFile(o.options.OnionKeyFile, []byte(keyData), 0644)
return err
}
func (o *OnionListener) initOnion() error {
//var serviceId string
var keyType, keyContent string
var err error
onionKeyContent := ""
onionKeyType := ""
if o.options.OnionKeyFile != "" {
onionKeyType, onionKeyContent, err = o.readOnionKeyFile()
}
err = o.controlAuth()
if err != nil {
return err
}
if onionKeyType != "" {
o.onionInfo, err = o.controller.AddOnion(o.options.OnionServicePort, o.options.LocalAddr, onionKeyType, onionKeyContent, false)
} else {
o.onionInfo, err = o.controller.AddOnion(o.options.OnionServicePort, o.options.LocalAddr, "", "", true)
}
if err != nil {
return fmt.Errorf("OnionListener: create hidden service fail: %s\n", err)
}
if o.options.OnionKeyFile != "" {
err = o.writeOnionKeyFile(keyType, keyContent)
if err != nil {
return fmt.Errorf("OnionListener: failed to write key file to disk: %s\n", err)
}
}
o.localListener, err = net.Listen("tcp", o.options.LocalAddr)
if err != nil {
return fmt.Errorf("OnionListener: local TCP listen error: %s\n", err)
}
return nil
}
func (o *OnionListener) Accept() (net.Conn, error) {
var conn net.Conn
var err error
if o.onionInfo.OnionId == "" {
return nil, fmt.Errorf("OnionListener: onion service not initialized.\n")
}
conn, err = o.localListener.Accept()
if err != nil {
return nil, fmt.Errorf("OnionListener: local TCP connection Accept failure: %s\n", err)
}
return conn, nil
}
func (o *OnionListener) Close() error {
err := o.controller.DeleteOnion(o.onionInfo.OnionId)
if err != nil {
return fmt.Errorf("OnionListener: DeleteHiddenService failure: %s\n", err)
}
return nil
}
func (o *OnionListener) Addr() net.Addr {
return OnionAddr{
network: "tor",
address: fmt.Sprintf("%s.onion:%d", o.onionInfo.OnionId, o.options.OnionServicePort),
}
}