From 9640ada8b7a823e605e45f319457340c3981b8d0 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 21 May 2026 20:30:17 -0700 Subject: [PATCH] Bind the ssh server to the newt ip --- clients/clients.go | 44 +++++++++++++++++++++++++++++++++++++++++++ main.go | 8 -------- nativessh/server.go | 46 +++++++++++++++++++++++++++++---------------- 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/clients/clients.go b/clients/clients.go index 3862160..6c4d968 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -19,6 +19,7 @@ import ( newtDevice "github.com/fosrl/newt/device" "github.com/fosrl/newt/holepunch" "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/nativessh" "github.com/fosrl/newt/netstack2" "github.com/fosrl/newt/network" "github.com/fosrl/newt/util" @@ -108,6 +109,8 @@ type WireGuardService struct { sharedBind *bind.SharedBind holePunchManager *holepunch.Manager useNativeInterface bool + // SSH server running on the clients' netstack + sshServer *sshServerHandle // Direct UDP relay from main tunnel to clients' WireGuard directRelayStop chan struct{} directRelayWg sync.WaitGroup @@ -211,6 +214,12 @@ func (s *WireGuardService) Close() { s.stopGetConfig = nil } + // Stop SSH server before tearing down the netstack + if s.sshServer != nil { + s.sshServer.stop() + s.sshServer = nil + } + // Flush access logs before tearing down the tunnel if s.tnet != nil { if ph := s.tnet.GetProxyHandler(); ph != nil { @@ -890,6 +899,13 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { logger.Error("Failed to start WireGuard tester server: %v", err) } + // Start the SSH server on the clients' netstack (port 22). + if h, sshErr := startSSHOnNetstack(s.tnet); sshErr != nil { + logger.Warn("nativessh: not starting SSH server on clients netstack: %v", sshErr) + } else { + s.sshServer = h + } + // Note: we already unlocked above, so don't use defer unlock return nil } @@ -1563,3 +1579,31 @@ func (s *WireGuardService) filterReadOnlyFields(config string) string { return strings.Join(filteredLines, "\n") } + +// sshServerHandle holds the listener so the SSH server can be stopped by +// closing it. +type sshServerHandle struct { + ln net.Listener +} + +func (h *sshServerHandle) stop() { + _ = h.ln.Close() +} + +// startSSHOnNetstack creates a TCP listener on port 22 of the clients' netstack +// and starts serving SSH connections on it in the background. The returned +// handle can be used to stop the server by closing the listener. +func startSSHOnNetstack(tnet *netstack2.Net) (*sshServerHandle, error) { + srv := nativessh.NewServer(nativessh.ServerConfig{}) + ln, err := tnet.ListenTCP(&net.TCPAddr{Port: 22}) + if err != nil { + return nil, fmt.Errorf("listen on netstack port 22: %w", err) + } + h := &sshServerHandle{ln: ln} + go func() { + if err := srv.Serve(ln); err != nil { + logger.Debug("nativessh: clients netstack server stopped: %v", err) + } + }() + return h, nil +} diff --git a/main.go b/main.go index 2ccfbda..cfeacf2 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,6 @@ import ( "github.com/fosrl/newt/docker" "github.com/fosrl/newt/healthcheck" "github.com/fosrl/newt/logger" - "github.com/fosrl/newt/nativessh" "github.com/fosrl/newt/proxy" "github.com/fosrl/newt/updates" "github.com/fosrl/newt/util" @@ -536,13 +535,6 @@ func runNewtMain(ctx context.Context) { } } - // Start native SSH server for testing (listens on :2222). - go func() { - srv := nativessh.NewServer(nativessh.ServerConfig{}) - if err := srv.ListenAndServe(); err != nil { - logger.Error("Native SSH server error: %v", err) - } - }() logger.GetLogger().SetLevel(loggerLevel) // Initialize telemetry after flags are parsed (so flags override env) diff --git a/nativessh/server.go b/nativessh/server.go index 0e5cbe9..d5c8b1f 100644 --- a/nativessh/server.go +++ b/nativessh/server.go @@ -58,42 +58,56 @@ func NewServer(cfg ServerConfig) *Server { return &Server{cfg: cfg} } -// ListenAndServe starts the SSH server and blocks until the listener is closed. -func (s *Server) ListenAndServe() error { +// buildSSHConfig loads keys/principals and builds the ssh.ServerConfig. +func (s *Server) buildSSHConfig() (*ssh.ServerConfig, error) { caKey, err := loadCAPublicKey(s.cfg.CAKeyPath) if err != nil { - return fmt.Errorf("load CA public key from %s: %w", s.cfg.CAKeyPath, err) + return nil, fmt.Errorf("load CA public key from %s: %w", s.cfg.CAKeyPath, err) } principals, err := loadPrincipals(s.cfg.PrincipalsPath) if err != nil { - return fmt.Errorf("load principals from %s: %w", s.cfg.PrincipalsPath, err) + return nil, fmt.Errorf("load principals from %s: %w", s.cfg.PrincipalsPath, err) } hostSigner, err := generateHostKey() if err != nil { - return fmt.Errorf("host key: %w", err) + return nil, fmt.Errorf("host key: %w", err) } - sshCfg := &ssh.ServerConfig{ + cfg := &ssh.ServerConfig{ PublicKeyCallback: makeCertAuthCallback(caKey, principals), } - sshCfg.AddHostKey(hostSigner) + cfg.AddHostKey(hostSigner) + return cfg, nil +} +// Serve accepts connections on ln and handles them. It returns when ln is +// closed or a non-temporary Accept error occurs. +func (s *Server) Serve(ln net.Listener) error { + sshCfg, err := s.buildSSHConfig() + if err != nil { + return err + } + log.Printf("nativessh: server listening on %s", ln.Addr()) + for { + conn, err := ln.Accept() + if err != nil { + return err + } + go s.handleConn(conn, sshCfg) + } +} + +// ListenAndServe starts the SSH server on the host network and blocks until +// the listener is closed. +func (s *Server) ListenAndServe() error { ln, err := net.Listen("tcp", s.cfg.ListenAddr) if err != nil { return fmt.Errorf("listen %s: %w", s.cfg.ListenAddr, err) } defer ln.Close() - log.Printf("nativessh: server listening on %s", s.cfg.ListenAddr) - - for { - conn, err := ln.Accept() - if err != nil { - return fmt.Errorf("accept: %w", err) - } - go s.handleConn(conn, sshCfg) - } + return s.Serve(ln) } func (s *Server) handleConn(conn net.Conn, cfg *ssh.ServerConfig) {