Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
View Source
var CopyToClipboardFlag = &cli.BoolFlag{ Name: "copy", Aliases: []string{"c"}, EnvVars: []string{"TUNL_COPY_ADDRESS"}, Usage: "Copies the public address to the clipboard", }
View Source
var DaemonCommand = &cli.Command{ Name: "daemon", Hidden: true, Flags: []cli.Flag{ &cli.StringFlag{ Name: "bind", Value: ":8080", }, &cli.StringSliceFlag{ Name: "tls-certs", }, &cli.StringFlag{ Name: "control", Value: "_.tunl.es", }, &cli.StringFlag{ Name: "domain", Value: "tunl.es", }, &cli.StringFlag{ Name: "address-template", Value: "https://{{.Id}}.{{.Domain}}", }, &cli.StringFlag{ Name: "sign-key", Value: xid.New().String(), }, &cli.StringFlag{ Name: "metrics.honeycomb.token", }, &cli.StringFlag{ Name: "metrics.honeycomb.name", Value: "tunl", }, }, Action: func(ctx *cli.Context) error { signKey := ctx.String("sign-key") if len(signKey) == 0 { logger.Error("sign-key cannot be empty") return nil } bind := ctx.String("bind") if len(bind) == 0 { logger.Error("bind flag value cannot be empty") return nil } if token := ctx.String("metrics.honeycomb.token"); len(token) > 0 { dataset := "tunl" if value := ctx.String("metrics.honeycomb.dataset"); len(value) > 0 { dataset = value } go honeycomb.Honeycomb(metrics.DefaultRegistry, 10*time.Second, token, dataset, []float64{0.50, 0.75, 0.95, 0.99}) logger.Info("honeycomb sink configurated", zap.String("dataset", dataset)) } tunnelCount := metrics.GetOrRegisterCounter("tunnel", nil) connectionCount := metrics.GetOrRegisterCounter("connections", nil) var listener net.Listener if certGlobs := ctx.StringSlice("tls-certs"); len(certGlobs) > 0 { certs, err := certs.LoadCertificates(certGlobs) if err != nil { logger.Error("load certificate error", zap.Error(err), zap.Strings("certs", certGlobs)) return nil } tlsListener, err := tls.Listen("tcp", bind, &tls.Config{ Certificates: certs, }) if err != nil { logger.Error("listen error failed to listen", zap.Error(err), zap.String("bind", bind)) return nil } listener = tlsListener } else { nonTlsListener, err := net.Listen("tcp", bind) if err != nil { logger.Error("listen error failed to listen", zap.Error(err), zap.String("bind", bind)) return nil } listener = nonTlsListener } logger.Debug("listener created", zap.String("address", listener.Addr().String())) mux, err := vhost.NewHTTPMuxer(listener, 30*time.Second) if err != nil { logger.Error("vhost mux creation error", zap.Error(err)) return nil } defer mux.Close() addresses := server.NewAddresses(logger, ctx.String("domain"), mux) failed := make(chan error) go func() { logger.Debug("creating control vhost", zap.String("control", ctx.String("control"))) control, err := mux.Listen(ctx.String("control")) if err != nil { failed <- errors.Wrap(err, "control vhost listener creation error") return } logger.Debug("control vhost created", zap.String("control", ctx.String("control"))) failed <- http.Serve(control, http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Method != http.MethodConnect { http.Error(response, "method not allowed", http.StatusMethodNotAllowed) return } conn, _, err := response.(http.Hijacker).Hijack() if err != nil { logger.Debug("http hijack error", zap.Error(err)) http.Error(response, "internal server error", http.StatusInternalServerError) return } defer conn.Close() tunlType := request.Header.Get("X-Tunl-Type") tunlToken := request.Header.Get("X-Tunl-Token") var address *server.PublicAddress if len(tunlToken) > 0 { token, err := verifyToken(signKey, tunlToken) if err != nil { logger.Info("invalid tunl token", zap.String("token", tunlToken), zap.Error(err)) http.Error(response, err.Error(), http.StatusInternalServerError) return } address, err = addresses.ClaimAddress(tunlType, token.Subject) if err != nil { logger.Info("address claim error", zap.String("address", token.Subject)) http.Error(response, err.Error(), http.StatusInternalServerError) return } } else { var err error address, err = addresses.NewAddress(tunlType) if err != nil { http.Error(response, err.Error(), http.StatusInternalServerError) return } } defer address.Close() token, err := createToken(signKey, address.Address) if err != nil { logger.Error("failed to create token", zap.Error(err)) http.Error(response, "internal server error", http.StatusInternalServerError) return } accept := &http.Response{ StatusCode: http.StatusOK, Header: http.Header{ "X-Tunl-Token": []string{token}, "X-Tunl-Address": []string{address.Address}, "X-Tunl-Version": []string{version.String()}, }, } if err := accept.Write(conn); err != nil { logger.Error("failed to write success response", zap.Error(err)) return } session, err := yamux.Server(conn, nil) if err != nil { logger.Debug("mux server creation error", zap.Error(err)) return } started := time.Now() defer func() { session.Close() logger.Debug("tunnel closed", zap.String("address", address.Address), zap.Duration("time-online", time.Since(started))) }() accepted := make(chan net.Conn) go func() { defer close(accepted) defer tunnelCount.Dec(1) tunnelCount.Inc(1) for { conn, err := address.Accept() if err != nil { logger.Debug("vhost accept error", zap.Error(err)) return } accepted <- conn } }() for { select { case <-session.CloseChan(): logger.Debug("session closed") return case conn, ok := <-accepted: if !ok { return } go func(public net.Conn) { defer public.Close() defer connectionCount.Dec(1) connectionCount.Inc(1) logger.Debug("accepted "+public.RemoteAddr().String(), zap.String("address", address.Address)) local, err := session.Open() if err != nil { logger.Debug("failed to open session", zap.Error(err)) return } defer local.Close() var work sync.WaitGroup work.Add(2) var in int64 var out int64 go func() { in, _ = io.Copy(local, public) work.Done() }() go func() { out, _ = io.Copy(public, local) work.Done() }() work.Wait() logger.Debug("connection copy finished", zap.Int64("in", in), zap.Int64("out", out), zap.Stringer("public", public.RemoteAddr()), zap.Stringer("local", local.RemoteAddr())) }(conn) } } })) }() go func() { for { conn, err := mux.NextError() if err != nil { switch err.(type) { case vhost.BadRequest: logger.Debug("vhost accept error: bad request", zap.Error(err)) break case vhost.NotFound: logger.Error("vhost mux reached unknown host") (&http.Response{ Status: "not found", StatusCode: http.StatusNotFound, }).Write(conn) break case vhost.Closed: logger.Error("vhost mux reached closed host") (&http.Response{ Status: "not found", StatusCode: http.StatusGone, }).Write(conn) break default: logger.Debug("unknown mux error", zap.Error(err)) } } if conn != nil { conn.Close() } } }() if err := <-failed; err != nil { logger.Error("fatal error", zap.Error(err)) } return nil }, }
View Source
var DirCommand = &cli.Command{ Name: "dir", Flags: []cli.Flag{ CopyToClipboardFlag, &cli.BoolFlag{ Name: "access-log", Value: true, }, &cli.StringFlag{ Name: "password", }, &cli.StringFlag{ Name: "basic-auth", Usage: "Adds HTTP basic access authentication", }, &cli.BoolFlag{ Name: "qr", Usage: "Print QR code of the public address", }, }, Usage: "Expose a directory via a public http address", ArgsUsage: "[dir]", Action: func(ctx *cli.Context) error { dir := ctx.Args().First() if len(dir) == 0 { dir = "." } absDir, err := filepath.Abs(dir) if err != nil { return cli.Exit("invalid dir: "+err.Error(), 1) } stat, err := os.Stat(absDir) if err != nil { if os.IsNotExist(err) { return cli.Exit("directory doesn't exist", 1) } return cli.Exit(err.Error(), 1) } if !stat.IsDir() { return cli.Exit(dir+" not a directory", 1) } host := ctx.String("host") if len(host) == 0 { fmt.Print("Host cannot be empty\nSee --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return cli.Exit("Host cannot be empty.", 1) } hostURL, err := url.Parse(host) if err != nil { fmt.Printf("Host value invalid: %v\nSee --host flag for more information.\n\n", err) cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } hostnameWithoutPort := hostURL.Hostname() if len(hostnameWithoutPort) == 0 { fmt.Print("Host hostname cannot be empty, see --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } handler := http.FileServer(http.Dir(absDir)) if password := ctx.String("password"); len(password) > 0 { next := handler var store = sessions.NewCookieStore([]byte("foobar")) handler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session, _ := store.Get(r, "tunl-pass") if session.Values["pass"] == password { next.ServeHTTP(rw, r) return } if r.Method == http.MethodPost { if r.ParseForm(); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } pass := r.FormValue("password") if pass != password { templates.Password(rw, templates.PasswordInput{ Message: "invalid password", }) return } session.Values["pass"] = pass session.Save(r, rw) http.Redirect(rw, r, "/", http.StatusSeeOther) return } templates.Password(rw, templates.PasswordInput{}) }) } if basicAuth := ctx.String("basic-auth"); len(basicAuth) > 0 { split := strings.Split(basicAuth, ":") if len(split) != 2 { return cli.Exit("invalid basic-auth value", 1) } user := split[0] password := split[1] if len(user) == 0 { return cli.Exit("invalid basic-auth value: empty user", 1) } if len(password) == 0 { return cli.Exit("invalid basic-auth value: empty password", 1) } handler = httpauth.SimpleBasicAuth(user, password)(handler) } if ctx.Bool("access-log") { handler = handlers.LoggingHandler(os.Stderr, handler) } tunnel, err := tunnel.OpenHTTP(ctx.Context, zap.NewNop(), hostURL) if err != nil { return cli.Exit(err.Error(), 18) } PrintTunnel(ctx, tunnel.Address(), absDir) go func() { for { select { case state := <-tunnel.StateChanges(): println(state) case version := <-tunnel.NewVersions(): current, err := semver.NewVersion(version.String()) if err == nil { if current.LessThan(&version) { println("new version available: " + version.String()) } } } } }() if err := http.Serve(tunnel, handler); err != nil { return err } return nil }, }
View Source
var DockerCommand = &cli.Command{ Name: "docker", ArgsUsage: "<container>[:<port>]", Flags: []cli.Flag{ CopyToClipboardFlag, &cli.BoolFlag{ Name: "qr", Usage: "Print QR code of the public address", }, &cli.BoolFlag{ Name: "copy-address", Usage: "Copies the public address to the clipboard", }, }, Usage: "Expose a docker container port via a public address", BashComplete: cli.BashCompleteFunc(func(ctx *cli.Context) { docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return } args := filters.NewArgs() if ctx.Args().Present() { args.FuzzyMatch("name", ctx.Args().First()) } containers, err := docker.ContainerList(context.Background(), types.ContainerListOptions{Filters: args}) if err != nil { return } for _, container := range containers { for _, name := range container.Names { fmt.Println(name) } fmt.Println(container.ID) } }), Action: func(ctx *cli.Context) error { containerAndPort := ctx.Args().First() if len(containerAndPort) == 0 { fmt.Print("Missing container argument.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return cli.Exit("Container cannot be empty.", 1) } docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } containerSpec, portSpec, err := net.SplitHostPort(containerAndPort) if err != nil { fmt.Print("Invalid container argument\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return cli.Exit(err.Error(), 1) } container, err := docker.ContainerInspect(context.Background(), containerSpec) if err != nil { return cli.Exit(err.Error(), 1) } var port int if len(portSpec) > 0 { parsed, err := strconv.Atoi(portSpec) if err != nil { return cli.Exit("Invalid port: "+portSpec, 1) } port = parsed } else { if exposedPorts := container.Config.ExposedPorts; len(exposedPorts) > 0 { for exposedPort := range exposedPorts { port = exposedPort.Int() } } else { return cli.Exit("Missing port argument and no exposed ports found in container", 1) } } host := ctx.String("host") if len(host) == 0 { fmt.Print("Host cannot be empty\nSee --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return cli.Exit("Host cannot be empty.", 1) } hostURL, err := url.Parse(host) if err != nil { fmt.Printf("Host value invalid: %v\nSee --host flag for more information.\n\n", err) cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } hostnameWithoutPort := hostURL.Hostname() if len(hostnameWithoutPort) == 0 { fmt.Print("Host hostname cannot be empty, see --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } tunnel, err := tunnel.OpenTCP(ctx.Context, zap.NewNop(), hostURL) if err != nil { return cli.Exit(err.Error(), 18) } PrintTunnel(ctx, tunnel.Address(), fmt.Sprintf("%s:%v", container.Name[1:], port)) go func() { for state := range tunnel.StateChanges() { println(state) } }() for { conn, err := tunnel.Accept() if err != nil { return cli.Exit("accept error: "+err.Error(), 1) } fmt.Println(conn.RemoteAddr()) go func(conn net.Conn) { defer conn.Close() target, err := net.Dial("tcp", fmt.Sprintf("%s:%v", container.NetworkSettings.IPAddress, port)) if err != nil { println(err.Error()) return } var work sync.WaitGroup work.Add(2) go func() { defer work.Done() io.Copy(conn, target) }() go func() { defer work.Done() io.Copy(target, conn) }() work.Wait() }(conn) } }, }
View Source
var FilesCommand = &cli.Command{ Name: "files", Flags: []cli.Flag{ CopyToClipboardFlag, &cli.BoolFlag{ Name: "access-log", Value: true, }, &cli.BoolFlag{ Name: "qr", Usage: "Print QR code of the public address", }, }, Hidden: true, Usage: "Expose a directory via a public http address", ArgsUsage: "[dir]", Action: func(ctx *cli.Context) error { println("FILES IS DEPRECATED AND WILL BE REMOVED SOON, USE DIR COMMAND") return DirCommand.Action(ctx) }, }
View Source
var HttpCommand = &cli.Command{ Name: "http", Flags: []cli.Flag{ CopyToClipboardFlag, &cli.BoolFlag{ Name: "access-log", Usage: "Print http requests in Apache Log format to stderr", Value: true, }, &cli.StringFlag{ Name: "basic-auth", Usage: "Adds HTTP basic access authentication", }, &cli.BoolFlag{ Name: "insecure", Usage: "Skip TLS verification for local address (this does not effect TLS between the tunl client and server or the public address)", Value: true, }, &cli.BoolFlag{ Name: "qr", Usage: "Print QR code of the public address", }, }, ArgsUsage: "<url>", Usage: "Expose a HTTP service via a public address", Action: func(ctx *cli.Context) error { var targetURL *url.URL target := ctx.Args().First() if len(target) == 0 { fmt.Fprint(os.Stderr, "You must specify the <url> argument\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) } if !strings.Contains(target, "://") { if strings.HasPrefix(target, ":") { target = target[1:] } if port, err := strconv.Atoi(target); err == nil { target = fmt.Sprintf("http://localhost:%v", port) } else { target = "http://" + target } } parsed, err := url.Parse(target) if err != nil { fmt.Fprintf(os.Stderr, "Invalid <url> argument value: %v\n\n", err) cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) } targetURL = parsed proxy := httputil.NewSingleHostReverseProxy(targetURL) if ctx.Bool("insecure") { proxy.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } originalDirector := proxy.Director proxy.Director = func(request *http.Request) { originalDirector(request) request.Host = targetURL.Host } host := ctx.String("host") if len(host) == 0 { fmt.Fprint(os.Stderr, "Host cannot be empty\nSee --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return cli.Exit("Host cannot be empty.", 1) } hostURL, err := url.Parse(host) if err != nil { fmt.Fprintf(os.Stderr, "Host value invalid: %v\nSee --host flag for more information.\n\n", err) cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } hostnameWithoutPort := hostURL.Hostname() if len(hostnameWithoutPort) == 0 { fmt.Fprintf(os.Stderr, "Host hostname cannot be empty, see --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } tunnel, err := tunnel.OpenHTTP(ctx.Context, zap.NewNop(), hostURL) if err != nil { return cli.Exit(err.Error(), 18) } PrintTunnel(ctx, tunnel.Address(), target) handler := handlers.LoggingHandler(os.Stdout, proxy) proxy.ErrorHandler = func(response http.ResponseWriter, request *http.Request, err error) { hostname, _ := os.Hostname() if len(hostname) == 0 { hostname = "<unknown>" } fmt.Println(err) var unwrapped = err for next := errors.Unwrap(unwrapped); next != nil; next = errors.Unwrap(unwrapped) { unwrapped = next } response.WriteHeader(http.StatusBadGateway) templates.HttpClientError(response, templates.HttpClientErrorInput{ RemoteAddress: tunnel.Address(), LocalHostname: hostname, LocalAddress: target, TunlClientVersion: ctx.App.Version, ErrMessage: unwrapped.Error(), ErrType: fmt.Sprintf("%T", err), ErrDetails: err.Error(), Year: time.Now().Year(), }) } if basicAuth := ctx.String("basic-auth"); len(basicAuth) > 0 { split := strings.Split(basicAuth, ":") if len(split) != 2 { return cli.Exit("invalid basic-auth value", 1) } user := split[0] password := split[1] if len(user) == 0 { return cli.Exit("invalid basic-auth value: empty user", 1) } if len(password) == 0 { return cli.Exit("invalid basic-auth value: empty password", 1) } handler = httpauth.SimpleBasicAuth(user, password)(handler) } if err := http.Serve(tunnel, handler); err != nil { return cli.Exit("fatal error: "+err.Error(), 1) } return nil }, }
View Source
var TcpCommand = &cli.Command{ Name: "tcp", Flags: []cli.Flag{ CopyToClipboardFlag, &cli.BoolFlag{ Name: "access-log", Value: true, }, }, Usage: "Expose a TCP service via a public address", ArgsUsage: "<host:port>", Action: func(ctx *cli.Context) error { target := ctx.Args().First() if len(target) == 0 { fmt.Fprint(os.Stderr, "You must specify the <url> argument\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) } host := ctx.String("host") if len(host) == 0 { fmt.Fprint(os.Stderr, "Host cannot be empty\nSee --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return cli.Exit("Host cannot be empty.", 1) } hostURL, err := url.Parse(host) if err != nil { fmt.Fprintf(os.Stderr, "Host value invalid: %v\nSee --host flag for more information.\n\n", err) cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } hostnameWithoutPort := hostURL.Hostname() if len(hostnameWithoutPort) == 0 { fmt.Fprintf(os.Stderr, "Host hostname cannot be empty, see --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } tunnel, err := tunnel.OpenTCP(ctx.Context, zap.NewNop(), hostURL) if err != nil { return cli.Exit(err.Error(), 18) } PrintTunnel(ctx, tunnel.Address(), ctx.Args().First()) for { conn, err := tunnel.Accept() if err != nil { return cli.Exit("fatal error: "+err.Error(), 1) } fmt.Println(conn.RemoteAddr()) go func(conn net.Conn) { defer conn.Close() target, err := net.Dial("tcp", ctx.Args().First()) if err != nil { println(err.Error()) return } var work sync.WaitGroup work.Add(2) go func() { defer work.Done() io.Copy(conn, target) }() go func() { defer work.Done() io.Copy(target, conn) }() work.Wait() }(conn) } }, }
View Source
var VersionCommand = &cli.Command{ Name: "version", Usage: "Print version information", Action: func(ctx *cli.Context) error { cli.VersionPrinter(ctx) return nil }, }
View Source
var WebdavCommand = &cli.Command{ Name: "webdav", Flags: []cli.Flag{ CopyToClipboardFlag, &cli.BoolFlag{ Name: "access-log", Value: true, }, &cli.StringFlag{ Name: "basic-auth", Usage: "Adds HTTP basic access authentication", }, &cli.BoolFlag{ Name: "qr", Usage: "Print QR code of the public address", }, }, Usage: "Expose a directory via a public webdav address", ArgsUsage: "[dir]", Action: func(ctx *cli.Context) error { dir := ctx.Args().First() if len(dir) == 0 { dir = "." } absDir, err := filepath.Abs(dir) if err != nil { return cli.Exit("invalid dir: "+err.Error(), 1) } stat, err := os.Stat(absDir) if err != nil { if os.IsNotExist(err) { return cli.Exit("directory doesn't exist", 1) } return cli.Exit(err.Error(), 1) } if !stat.IsDir() { return cli.Exit(dir+" not a directory", 1) } host := ctx.String("host") if len(host) == 0 { fmt.Print("Host cannot be empty\nSee --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return cli.Exit("Host cannot be empty.", 1) } hostURL, err := url.Parse(host) if err != nil { fmt.Printf("Host value invalid: %v\nSee --host flag for more information.\n\n", err) cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } hostnameWithoutPort := hostURL.Hostname() if len(hostnameWithoutPort) == 0 { fmt.Print("Host hostname cannot be empty, see --host flag for more information.\n\n") cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) return nil } handler := http.Handler(&webdav.Handler{ FileSystem: webdav.Dir(absDir), LockSystem: webdav.NewMemLS(), }) if basicAuth := ctx.String("basic-auth"); len(basicAuth) > 0 { split := strings.Split(basicAuth, ":") if len(split) != 2 { return cli.Exit("invalid basic-auth value", 1) } user := split[0] password := split[1] if len(user) == 0 { return cli.Exit("invalid basic-auth value: empty user", 1) } if len(password) == 0 { return cli.Exit("invalid basic-auth value: empty password", 1) } handler = httpauth.SimpleBasicAuth(user, password)(handler) } if ctx.Bool("access-log") { handler = handlers.LoggingHandler(os.Stderr, handler) } tunnel, err := tunnel.OpenHTTP(ctx.Context, zap.NewNop(), hostURL) if err != nil { return cli.Exit(err.Error(), 18) } PrintTunnel(ctx, tunnel.Address(), absDir) go func() { for state := range tunnel.StateChanges() { println(state) } }() if err := http.Serve(tunnel, handler); err != nil { return err } return nil }, }
Functions ¶
func CopyAddressToClipboardIfRequired ¶
func CopyAddressToClipboardIfRequired(ctx *cli.Context, address string)
func PrintTunnel ¶
PrintTunnel prints public address to stdout and target to stderr
Types ¶
This section is empty.
Click to show internal directories.
Click to hide internal directories.