diff --git a/cmd_onion.go b/cmd_onion.go
new file mode 100644
index 0000000..db0e8d0
--- /dev/null
+++ b/cmd_onion.go
@@ -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
+// 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
+}
diff --git a/examples/onion/onion.go b/examples/onion/onion.go
new file mode 100644
index 0000000..7187b8f
--- /dev/null
+++ b/examples/onion/onion.go
@@ -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
+// 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)
+ }
+}
diff --git a/utils/connection.go b/utils/connection.go
new file mode 100644
index 0000000..2750a63
--- /dev/null
+++ b/utils/connection.go
@@ -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
+// 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),
+ }
+}