diff --git a/README.md b/README.md index d78a095..2210f2f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # tunkit - ssh tunnel tooling - Passwordless authentication for the browser using SSH local forwarding. -- Pub/sub system using SSH remote forwarding. - Implemented as [wish](https://github.com/charmbracelet/wish) middleware. # Passwordless authentication @@ -51,22 +50,6 @@ docker pull localhost:1338/alpine:latest We built this library to support [imgs.sh](https://pico.sh/imgs): a private docker registry leveraging SSH tunnels. -# Pub/sub system - -Use an SSH tunnels for "webhooks": - -- Integrate the publisher middleware into an SSH server -- A user can start an http server on localhost -- User can initial an SSH remote tunnel to SSH server -- Publisher emits events by `http.Get` the user's local http server - -## Why? - -The biggest benefit is the user's http server is not public. There's zero -concern for malicious actors or bots trying to hit a user's event endpoints. -This dramatically reduces the infrastructure requirements for the end-user. They -just need to start an http server and initial a tunnel to a service. - # Examples Checkout our [cmd/](./cmd/) folder for more examples. diff --git a/cmd/pubsub/pub/cli.go b/cmd/pubsub/pub/cli.go deleted file mode 100644 index c477a0d..0000000 --- a/cmd/pubsub/pub/cli.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "strings" - - "github.com/charmbracelet/ssh" - "github.com/charmbracelet/wish" - "github.com/picosh/tunkit" - gossh "golang.org/x/crypto/ssh" -) - -func keyForSha256(pk ssh.PublicKey) string { - return gossh.FingerprintSHA256(pk) -} - -func CliMiddleware(handler tunkit.PubSub) wish.Middleware { - log := handler.GetLogger() - - return func(next ssh.Handler) ssh.Handler { - return func(sesh ssh.Session) { - _, _, activePty := sesh.Pty() - if activePty { - next(sesh) - return - } - - args := sesh.Command() - forwards := handler.GetForwards() - - cmd := strings.TrimSpace(args[0]) - if cmd == "emit" { - msg := args[1] - - if len(forwards) == 0 { - wish.Println(sesh, "no listeners") - log.Info("no listeners") - return - } - - for _, rf := range forwards { - addr := rf.Listener.Addr() - furl := fmt.Sprintf( - "http://%s?msg=%s", - addr.String(), - msg, - ) - logger := log.With( - "pubkey", keyForSha256(rf.Pubkey), - "addr", addr, - "msg", msg, - "url", furl, - ) - - wish.Printf(sesh, "[GET] %s\n", furl) - logger.Info("emitting to listener") - - _, err := http.Get(furl) - if err != nil { - logger.Error("unable send message", "err", err) - } - } - return - } else if cmd == "ls" { - if len(forwards) == 0 { - log.Info("no listeners") - wish.Println(sesh, "no listeners") - return - } - - for _, rf := range forwards { - addr := rf.Listener.Addr() - pk := keyForSha256(rf.Pubkey) - logger := log.With( - "pubkey", pk, - "addr", addr, - ) - logger.Info("listener") - wish.Println(sesh, fmt.Sprintf("addr:%s pubkey:%s", addr, pk)) - } - return - } else { - next(sesh) - return - } - } - } -} diff --git a/cmd/pubsub/pub/main.go b/cmd/pubsub/pub/main.go deleted file mode 100644 index 39e40db..0000000 --- a/cmd/pubsub/pub/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log/slog" - "os" - "os/signal" - "syscall" - "time" - - "github.com/charmbracelet/ssh" - "github.com/charmbracelet/wish" - "github.com/picosh/tunkit" -) - -func authHandler(ctx ssh.Context, key ssh.PublicKey) bool { - return true -} - -func main() { - host := "0.0.0.0" - port := "2222" - - logger := slog.Default() - handler := tunkit.NewPubSubHandler(logger) - s, err := wish.NewServer( - wish.WithAddress(fmt.Sprintf("%s:%s", host, port)), - wish.WithHostKeyPath("ssh_data/term_info_ed25519"), - wish.WithPublicKeyAuth(authHandler), - tunkit.WithPubSub(handler), - wish.WithMiddleware(CliMiddleware(handler)), - ) - - if err != nil { - logger.Error("could not create server", "err", err) - } - - done := make(chan os.Signal, 1) - signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - logger.Info("starting SSH server", "host", host, "port", port) - go func() { - if err = s.ListenAndServe(); err != nil { - logger.Error("serve error", "err", err) - os.Exit(1) - } - }() - - <-done - logger.Info("stopping SSH server") - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer func() { cancel() }() - if err := s.Shutdown(ctx); err != nil { - logger.Error("shutdown", "err", err) - os.Exit(1) - } -} diff --git a/cmd/pubsub/sub/main.go b/cmd/pubsub/sub/main.go deleted file mode 100644 index e7231e0..0000000 --- a/cmd/pubsub/sub/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "log/slog" - "net/http" -) - -func main() { - host := "0.0.0.0" - port := "3000" - logger := slog.Default() - - router := http.NewServeMux() - router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - body := fmt.Sprintf("received event: (path:%s, query:%s)", r.URL.Path, r.URL.Query()) - _, err := w.Write([]byte(body)) - logger.Info("response", "body", body) - if err != nil { - logger.Error("error writing response", "err", err) - } - }) - - listen := fmt.Sprintf("%s:%s", host, port) - logger.Info("server running\n", "url", listen) - err := http.ListenAndServe( - listen, - router, - ) - if err != nil { - logger.Error("http serve", "err", err) - } -} diff --git a/pubsub-handler.go b/pubsub-handler.go deleted file mode 100644 index a99b07d..0000000 --- a/pubsub-handler.go +++ /dev/null @@ -1,174 +0,0 @@ -package tunkit - -import ( - "bytes" - "io" - "log/slog" - "net" - "strconv" - "sync" - - "github.com/charmbracelet/ssh" - gossh "golang.org/x/crypto/ssh" - "golang.org/x/exp/maps" -) - -type remoteForwardRequest struct { - BindAddr string - BindPort uint32 -} - -type remoteForwardSuccess struct { - BindPort uint32 -} - -type remoteForwardCancelRequest struct { - BindAddr string - BindPort uint32 -} - -type remoteForwardChannelData struct { - DestAddr string - DestPort uint32 - OriginAddr string - OriginPort uint32 -} - -type RemoteForwards struct { - Listener net.Listener - Pubkey ssh.PublicKey -} - -// PubSubHandler can be enabled by creating a PubSubHandler and -// adding the HandleSSHRequest callback to the server's RequestHandlers under -// tcpip-forward and cancel-tcpip-forward. -type PubSubHandler struct { - Logger *slog.Logger - sync.Mutex - forwards map[string]*RemoteForwards -} - -func NewPubSubHandler(logger *slog.Logger) *PubSubHandler { - return &PubSubHandler{ - Logger: logger, - } -} - -var forwardedTCPChannelType = "forwarded-tcpip" - -func (h *PubSubHandler) GetForwards() []*RemoteForwards { - return maps.Values(h.forwards) -} - -func (h *PubSubHandler) GetLogger() *slog.Logger { - return h.Logger -} - -func (h *PubSubHandler) GetForwardsByPubkey(pubkey ssh.PublicKey) []net.Listener { - list := []net.Listener{} - for _, v := range h.forwards { - if bytes.Equal(v.Pubkey.Marshal(), pubkey.Marshal()) { - list = append(list, v.Listener) - } - } - return list -} - -func (h *PubSubHandler) HandleRequest(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) { - logger := h.GetLogger() - h.Lock() - if h.forwards == nil { - h.forwards = make(map[string]*RemoteForwards) - } - h.Unlock() - conn := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn) - switch req.Type { - case "tcpip-forward": - var reqPayload remoteForwardRequest - if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { - logger.Error("failed to parse request payload", "err", err) - return false, []byte{} - } - addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) - ln, err := net.Listen("tcp", addr) - if err != nil { - logger.Error("failed create net listener", "err", err) - return false, []byte{} - } - _, destPortStr, _ := net.SplitHostPort(ln.Addr().String()) - destPort, _ := strconv.Atoi(destPortStr) - pubkey, _ := ctx.Value(ssh.ContextKeyPublicKey).(ssh.PublicKey) - remoteForward := RemoteForwards{ - Listener: ln, - Pubkey: pubkey, - } - h.Lock() - h.forwards[addr] = &remoteForward - h.Unlock() - go func() { - <-ctx.Done() - h.Lock() - rf, ok := h.forwards[addr] - h.Unlock() - if ok { - rf.Listener.Close() - } - }() - go func() { - for { - c, err := ln.Accept() - if err != nil { - logger.Error("failed accept channel", "err", err) - break - } - originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String()) - originPort, _ := strconv.Atoi(orignPortStr) - payload := gossh.Marshal(&remoteForwardChannelData{ - DestAddr: reqPayload.BindAddr, - DestPort: uint32(destPort), - OriginAddr: originAddr, - OriginPort: uint32(originPort), - }) - go func() { - ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload) - if err != nil { - c.Close() - return - } - go gossh.DiscardRequests(reqs) - go func() { - defer ch.Close() - defer c.Close() - _, _ = io.Copy(ch, c) - }() - go func() { - defer ch.Close() - defer c.Close() - _, _ = io.Copy(c, ch) - }() - }() - } - h.Lock() - delete(h.forwards, addr) - h.Unlock() - }() - return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)}) - - case "cancel-tcpip-forward": - var reqPayload remoteForwardCancelRequest - if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil { - logger.Error("failed parse payload", "err", err) - return false, []byte{} - } - addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort))) - h.Lock() - rf, ok := h.forwards[addr] - h.Unlock() - if ok { - rf.Listener.Close() - } - return true, nil - default: - return false, nil - } -} diff --git a/pubsub.go b/pubsub.go deleted file mode 100644 index 6c6dbd0..0000000 --- a/pubsub.go +++ /dev/null @@ -1,24 +0,0 @@ -package tunkit - -import ( - "log/slog" - - "github.com/charmbracelet/ssh" - gossh "golang.org/x/crypto/ssh" -) - -type PubSub interface { - GetLogger() *slog.Logger - GetForwards() []*RemoteForwards - HandleRequest(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) -} - -func WithPubSub(handler PubSub) ssh.Option { - return func(serv *ssh.Server) error { - serv.RequestHandlers = map[string]ssh.RequestHandler{ - "tcpip-forward": handler.HandleRequest, - "cancel-tcpip-forward": handler.HandleRequest, - } - return nil - } -}