Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ This will:
```

Notes:
- **Port 80 and 443 must be open** - Let's Encrypt uses HTTP-01 validation on port 80 to issue certificates. The server will automatically start an HTTP listener on port 80 for this purpose.
- If you omit the password argument, the installer will prompt for it.
- If you need to reconfigure, edit the systemd service and restart it (sudo systemctl daemon-reload && sudo systemctl restart chissl).
- **Troubleshooting certs**: If certificate renewal fails, try clearing the cache: `rm -rf ~/.cache/chisel/` (or `/root/.cache/chisel/` if running as root).

Manual download option (no installer):
- Download the Linux server binary that matches your architecture from Releases:
Expand Down
43 changes: 38 additions & 5 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,16 +437,49 @@ func (s *Server) StartContext(ctx context.Context, host, port string) error {
if s.listeners != nil && s.config.TlsConf != nil {
s.listeners.UpdateTLSConfig(s.config.TlsConf)
s.Debugf("Updated listener manager with TLS config")
// Now restore active listeners from database with proper TLS config
s.restoreListeners()
}
// Update multicast manager with TLS and restore enabled multicasts
// Update multicast manager with TLS config
if s.multicasts != nil && s.config.TlsConf != nil {
s.multicasts.UpdateTLSConfig(s.config.TlsConf)
s.Debugf("Updated multicast manager with TLS config")
s.restoreMulticasts()

}

// Restore listeners/multicasts after a short delay to ensure:
// 1. HTTP-01 challenge server on :80 is ready
// 2. Main HTTPS server is accepting connections
// 3. TLS cert can be fetched on first connection if needed
go func() {
// Wait for HTTP-01 server and main server to be ready
time.Sleep(3 * time.Second)

// Pre-warm TLS certificate (non-blocking, with timeout)
if s.config.TlsConf != nil && len(s.config.TLS.Domains) > 0 {
s.Infof("Pre-warming TLS certificate for domains: %v", s.config.TLS.Domains)
done := make(chan error, 1)
go func() {
done <- s.prewarmTLSCert()
}()
select {
case err := <-done:
if err != nil {
s.Infof("Warning: TLS certificate pre-warm failed: %v (will retry on first connection)", err)
} else {
s.Infof("TLS certificate ready")
}
case <-time.After(60 * time.Second):
s.Infof("Warning: TLS certificate pre-warm timed out (will retry on first connection)")
}
}

// Now restore active listeners from database with proper TLS config
if s.listeners != nil && s.config.TlsConf != nil {
s.restoreListeners()
}
// Restore enabled multicasts
if s.multicasts != nil && s.config.TlsConf != nil {
s.restoreMulticasts()
}
}()
h := http.Handler(http.HandlerFunc(s.handleClientHandler))
if s.Debug {
o := requestlog.DefaultOptions
Expand Down
59 changes: 58 additions & 1 deletion server/server_listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"crypto/x509"
"errors"
"net"
"net/http"
"os"
"os/user"
"path/filepath"

"github.com/NextChapterSoftware/chissl/share/settings"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)

Expand Down Expand Up @@ -64,12 +66,19 @@ func (s *Server) tlsLetsEncrypt(domains []string) *tls.Config {
//prepare cert manager
m := &autocert.Manager{
Prompt: func(tosURL string) bool {
s.Infof("Accepting LetsEncrypt TOS and fetching certificate...")
s.Infof("Accepting LetsEncrypt TOS and fetching certificate (TOS: %s)...", tosURL)
return true
},
Email: settings.Env("LE_EMAIL"),
HostPolicy: autocert.HostWhitelist(domains...),
}
// Use staging environment if CHISEL_LE_STAGING is set (for testing)
if settings.Env("LE_STAGING") != "" {
s.Infof("Using Let's Encrypt STAGING environment (certs won't be trusted)")
m.Client = &acme.Client{
DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory",
}
}
//configure file cache
c := settings.Env("LE_CACHE")
if c == "" {
Expand All @@ -85,6 +94,26 @@ func (s *Server) tlsLetsEncrypt(domains []string) *tls.Config {
s.Infof("LetsEncrypt cache directory %s", c)
m.Cache = autocert.DirCache(c)
}

// Start HTTP-01 challenge listener on port 80 for ACME validation
go func() {
s.Infof("Starting HTTP-01 challenge listener on :80 for LetsEncrypt validation")
// Wrap the handler to log challenge requests
challengeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if len(r.URL.Path) > 28 && r.URL.Path[:28] == "/.well-known/acme-challenge/" {
s.Infof("ACME challenge request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
}
m.HTTPHandler(nil).ServeHTTP(w, r)
})
httpServer := &http.Server{
Addr: ":80",
Handler: challengeHandler,
}
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
s.Infof("HTTP-01 challenge listener error: %v", err)
}
}()

//return lets-encrypt tls config
return m.TLSConfig()
}
Expand Down Expand Up @@ -149,3 +178,31 @@ func addPEMFile(path string, pool *x509.CertPool) error {
}
return nil
}

// prewarmTLSCert triggers certificate fetching by making a test TLS connection
// This ensures the Let's Encrypt certificate is fetched before other listeners start
func (s *Server) prewarmTLSCert() error {
if s.config.TlsConf == nil || s.config.TlsConf.GetCertificate == nil {
// No dynamic certificate (not Let's Encrypt), nothing to pre-warm
return nil
}

// Use the first domain to trigger certificate fetch
if len(s.config.TLS.Domains) == 0 {
return nil
}
domain := s.config.TLS.Domains[0]

// Trigger GetCertificate by calling it directly with a ClientHelloInfo
hello := &tls.ClientHelloInfo{
ServerName: domain,
}
cert, err := s.config.TlsConf.GetCertificate(hello)
if err != nil {
return err
}
if cert == nil {
return errors.New("no certificate returned")
}
return nil
}
Loading