diff --git a/examples/listener/listener.go b/examples/listener/listener.go
new file mode 100644
index 0000000..d017f43
--- /dev/null
+++ b/examples/listener/listener.go
@@ -0,0 +1,51 @@
+// Listener example.
+//
+// To the extent possible under law, Yawning Angel waived all copyright
+// and related or neighboring rights to bulb, using the creative
+// commons "cc0" public domain dedication. See LICENSE or
+// for full details.
+
+package main
+
+import (
+ "io"
+ "log"
+ "net/http"
+
+ "github.com/yawning/bulb"
+)
+
+func onionServer(w http.ResponseWriter, req *http.Request) {
+ io.WriteString(w, "hello, onion world!\n")
+}
+
+func main() {
+ // Connect to a running tor instance.
+ // * TCP: c, err := bulb.Dial("tcp4", "127.0.0.1:9051")
+ c, err := bulb.Dial("unix", "/var/run/tor/control")
+ 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)
+ }
+
+ // Create an ephemeral port 80 Onion Service.
+ l, err := c.Listener(80, nil)
+ if err != nil {
+ log.Fatalf("Failed to get Listener: %v", err)
+ }
+ defer l.Close()
+
+ log.Printf("Listener: %s", l.Addr())
+ http.HandleFunc("/", onionServer)
+ http.Serve(l, nil)
+}
diff --git a/listener.go b/listener.go
new file mode 100644
index 0000000..b7ddfa8
--- /dev/null
+++ b/listener.go
@@ -0,0 +1,87 @@
+// listener.go - Tor backed net.Listener.
+//
+// To the extent possible under law, Yawning Angel waived all copyright
+// and related or neighboring rights to bulb, using the creative
+// commons "cc0" public domain dedication. See LICENSE or
+// for full details.
+
+package bulb
+
+import (
+ "crypto"
+ "fmt"
+ "net"
+ "strconv"
+)
+
+type onionAddr struct {
+ info *OnionInfo
+ port uint16
+}
+
+func (a *onionAddr) Network() string {
+ return "tcp"
+}
+
+func (a *onionAddr) String() string {
+ return fmt.Sprintf("%s.onion:%d", a.info.OnionID, a.port)
+}
+
+type onionListener struct {
+ addr *onionAddr
+ ctrlConn *Conn
+ listener net.Listener
+}
+
+func (l *onionListener) Accept() (net.Conn, error) {
+ return l.listener.Accept()
+}
+
+func (l *onionListener) Close() (err error) {
+ if err = l.listener.Close(); err == nil {
+ // Only delete the onion once.
+ err = l.ctrlConn.DeleteOnion(l.addr.info.OnionID)
+ }
+ return err
+}
+
+func (l *onionListener) Addr() net.Addr {
+ return l.addr
+}
+
+// Listener returns a net.Listener backed by a Onion Service, optionally
+// having Tor generate an ephemeral private key. Regardless of the status of
+// the returned Listener, the Onion Service will be torn down when the control
+// connection is closed.
+//
+// WARNING: Only one port can be listened to per PrivateKey if this interface
+// is used. To bind to more ports, use the AddOnion call directly.
+func (c *Conn) Listener(port uint16, key crypto.PrivateKey) (net.Listener, error) {
+ const (
+ loopbackAddr = "127.0.0.1:0"
+ )
+
+ // Listen on the loopback interface.
+ tcpListener, err := net.Listen("tcp4", loopbackAddr)
+ if err != nil {
+ return nil, err
+ }
+ tAddr, ok := tcpListener.Addr().(*net.TCPAddr)
+ if !ok {
+ tcpListener.Close()
+ return nil, newProtocolError("failed to extract local port")
+ }
+
+ // Create the onion.
+ ports := []OnionPortSpec{{port, strconv.FormatUint((uint64)(tAddr.Port), 10)}}
+ oi, err := c.AddOnion(ports, key, key == nil)
+ if err != nil {
+ tcpListener.Close()
+ return nil, err
+ }
+
+ oa := &onionAddr{info: oi, port: port}
+ ol := &onionListener{addr: oa, ctrlConn: c, listener: tcpListener}
+
+ return ol, nil
+}