From 46d078dde1f95aab6d248f30fdc96c9768705f09 Mon Sep 17 00:00:00 2001 From: julien Date: Tue, 19 Aug 2025 17:28:39 +0200 Subject: [PATCH] up --- main.go | 265 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 153 insertions(+), 112 deletions(-) diff --git a/main.go b/main.go index 1415863..c21d4f0 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/gin-gonic/gin" @@ -24,117 +25,152 @@ import ( "golang.org/x/crypto/ssh" ) -// --------- CONFIG À ADAPTER ---------- - +// ====== Config ====== var ( - // Dossier racine SFTP (tu montes déjà ton volume ici dans Docker) - SFTPBaseDir = "upload" - - // Identifiants standards (utilisés si IP non autorisée) - LoginUser = "cangui2089" - LoginPass = "GHT30k7!" - - // IP(s) autorisées pour connexion SANS mot de passe - AllowedIPs = []string{ - "82.65.73.115", - } + SFTPBaseDir = "upload" // utilisé seulement pour vérif existence + logs + BindAddr = "0.0.0.0:2222" // adapte si tu veux binder sur une IP privée + LoginUser = "cangui2089" + LoginPass = "GHT30k7!" + AllowedIPs = []string{"82.65.73.115"} // IPs autorisées (pour pass bypass) + // Si tu veux fermer le service à toute IP non autorisée AVANT handshake, mets true : + OnlyAllowListedBeforeHandshake = false ) -// ------------------------------------- - -// ---------- SFTP Server ---------- - -// startSFTPServer démarre un serveur SFTP sur le port 2222 -// avec authentification par IP-allowlist et user/pass. -// Le dossier racine SFTP est défini par `base`. -// Les clés hôte sont générées ou chargées depuis le disque. -// Les IP autorisées peuvent se connecter sans mot de passe. -// Les autres doivent utiliser le login et mot de passe définis. -// Le serveur écoute sur le port 2222 et gère les connexions SFTP. -// Il utilise les clés hôte ed25519 et RSA pour la compatibilité. -// Les connexions TCP ont un keep-alive agressif pour éviter les coupures NAT. -// Les erreurs de connexion sont loguées, mais le serveur continue à accepter les connexions. -// Les requêtes SFTP sont traitées avec un serveur SFTP minimaliste. -// Les erreurs de traitement des requêtes sont loguées. -// Le serveur est conçu pour être lancé dans un goroutine séparé. -// Il vérifie que le dossier racine existe et est un répertoire valide. -// Les clés hôte sont stockées dans le même répertoire que le serveur SFTP. -// Le serveur utilise le protocole SSH pour établir les connexions SFTP. -// Il gère les canaux de session et les requêtes de sous-système SFTP. -// Les connexions sont acceptées en boucle infinie, avec des logs pour chaque connexion réussie ou échouée. -// Les erreurs de handshake SSH sont loguées et la connexion est fermée. -// Les canaux SFTP sont acceptés et traités pour servir les requêtes SFTP. -// Le serveur SFTP est initialisé avec des gestionnaires par défaut pour les opérations de fichier. -// Les erreurs lors de l'initialisation ou du traitement des requêtes SFTP sont loguées. -// Le serveur continue à accepter les connexions même après des erreurs, garantissant une disponibilité maximale. -// Il est recommandé de lancer cette fonction dans un goroutine séparé pour ne pas bloquer l'exécution principale du programme. - - -func startSFTPServer(base string) { - // --- helpers locaux pour charger/générer les clés hôte --- - loadOrCreateEd25519 := func(path string) (ssh.Signer, error) { - if _, err := os.Stat(path); err == nil { - b, err := os.ReadFile(path) - if err != nil { return nil, err } - return ssh.ParsePrivateKey(b) - } - _, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { return nil, err } - pkcs8, err := x509.MarshalPKCS8PrivateKey(priv) - if err != nil { return nil, err } - pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}) - if err := os.WriteFile(path, pemBytes, 0o600); err != nil { return nil, err } - return ssh.ParsePrivateKey(pemBytes) +// ====== Helpers ====== +func ipAllowed(addr string) bool { + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = addr } - loadOrCreateRSA := func(path string) (ssh.Signer, error) { - if _, err := os.Stat(path); err == nil { - b, err := os.ReadFile(path) - if err != nil { return nil, err } - return ssh.ParsePrivateKey(b) + for _, allow := range AllowedIPs { + if host == allow { + return true } - priv, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { return nil, err } - pkcs1 := x509.MarshalPKCS1PrivateKey(priv) - pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1}) - if err := os.WriteFile(path, pemBytes, 0o600); err != nil { return nil, err } - return ssh.ParsePrivateKey(pemBytes) } - ipAllowed := func(addr string) bool { - host, _, err := net.SplitHostPort(addr) - if err != nil { host = addr } - for _, allow := range AllowedIPs { - if host == allow { return true } - } + return false +} + +func isBenignHandshakeErr(err error) bool { + if err == nil { return false } + s := err.Error() + switch { + case strings.Contains(s, "unmarshal error for field Language of type disconnectMsg"), + strings.Contains(s, "connection reset by peer"), + strings.Contains(s, "no common algorithm"), + strings.Contains(s, "read: connection timed out"), + strings.Contains(s, "unexpected message"), + strings.Contains(s, "EOF"): + return true + } + return false +} - // --- vérifications et "ancrage" dans le dossier upload --- - absBase, _ := filepath.Abs(base) +func loadOrCreateEd25519(path string) (ssh.Signer, error) { + if _, err := os.Stat(path); err == nil { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return ssh.ParsePrivateKey(b) + } + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + pkcs8, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, err + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}) + if err := os.WriteFile(path, pemBytes, 0o600); err != nil { + return nil, err + } + return ssh.ParsePrivateKey(pemBytes) +} + +func loadOrCreateRSA(path string) (ssh.Signer, error) { + if _, err := os.Stat(path); err == nil { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return ssh.ParsePrivateKey(b) + } + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + pkcs1 := x509.MarshalPKCS1PrivateKey(priv) + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1}) + if err := os.WriteFile(path, pemBytes, 0o600); err != nil { + return nil, err + } + return ssh.ParsePrivateKey(pemBytes) +} + +// ====== Rate limit très simple ====== +type bucket struct { + tokens int + last time.Time +} +var rl = struct { + m map[string]*bucket + sync.Mutex +}{m: map[string]*bucket{}} + +func allow(ip string, ratePerMin, burst int) bool { + rl.Lock() + defer rl.Unlock() + now := time.Now() + b := rl.m[ip] + if b == nil { + b = &bucket{tokens: burst, last: now} + rl.m[ip] = b + } + elapsed := now.Sub(b.last).Minutes() + add := int(elapsed * float64(ratePerMin)) + if add > 0 { + if b.tokens+add > burst { + b.tokens = burst + } else { + b.tokens += add + } + b.last = now + } + if b.tokens <= 0 { + return false + } + b.tokens-- + return true +} + +// ====== SFTP Server (sans Chdir global) ====== +func startSFTPServer(baseDir, bindAddr string) { + absBase, _ := filepath.Abs(baseDir) if fi, err := os.Stat(absBase); err != nil || !fi.IsDir() { log.Fatalf("[SFTP] Le dossier %s est manquant ou invalide: %v", absBase, err) } - // if err := os.Chdir(absBase); err != nil { - // log.Fatalf("[SFTP] Chdir(%s): %v", absBase, err) - // } - // --- clés hôte : ed25519 (moderne) + RSA (compat) --- - signerED, err := loadOrCreateEd25519("sftp_host_ed25519.pem") - if err != nil { log.Fatalf("[SFTP] Host key ed25519 error: %v", err) } - signerRSA, err := loadOrCreateRSA("sftp_host_rsa.pem") - if err != nil { log.Fatalf("[SFTP] Host key RSA error: %v", err) } + signerED, err := loadOrCreateEd25519(filepath.Join(absBase, "sftp_host_ed25519.pem")) + if err != nil { + log.Fatalf("[SFTP] Host key ed25519 error: %v", err) + } + signerRSA, err := loadOrCreateRSA(filepath.Join(absBase, "sftp_host_rsa.pem")) + if err != nil { + log.Fatalf("[SFTP] Host key RSA error: %v", err) + } - // --- config SSH avec auth IP-allowlist et user/pass --- cfg := &ssh.ServerConfig{ + MaxAuthTries: 2, // réduire le bruteforce PasswordCallback: func(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { remote := meta.RemoteAddr().String() user := meta.User() - - // IP autorisée → on accepte même mot de passe vide if ipAllowed(remote) { log.Printf("[SFTP] Auth IP-allowlist OK from=%s user=%s (mdp vide accepté)", remote, user) return nil, nil } - // Sinon, exige user+pass if user == LoginUser && string(pass) == LoginPass { log.Printf("[SFTP] Auth OK from=%s user=%s", remote, user) return nil, nil @@ -146,25 +182,12 @@ func startSFTPServer(base string) { cfg.AddHostKey(signerED) cfg.AddHostKey(signerRSA) - // (option compat avancée) : décommente seulement si un client antique échoue encore - // cfg.Config = ssh.Config{ - // KeyExchanges: []string{ - // "curve25519-sha256", "curve25519-sha256@libssh.org", - // "diffie-hellman-group14-sha1", - // }, - // Ciphers: []string{ - // "chacha20-poly1305@openssh.com", - // "aes128-ctr","aes192-ctr","aes256-ctr", - // "aes128-cbc", - // }, - // MACs: []string{"hmac-sha2-256","hmac-sha2-512","hmac-sha1"}, - // } - - // --- listener TCP avec keep-alive agressif --- - ln, err := net.Listen("tcp", ":2222") - if err != nil { log.Fatalf("[SFTP] Listen: %v", err) } - log.Printf("[SFTP] Écoute sur sftp://%s@0.0.0.0:2222 (root=%s)", LoginUser, absBase) - log.Printf("[SFTP] IP sans mot de passe autorisées: %v", AllowedIPs) + ln, err := net.Listen("tcp", bindAddr) + if err != nil { + log.Fatalf("[SFTP] Listen(%s): %v", bindAddr, err) + } + log.Printf("[SFTP] Écoute sur sftp://%s@%s (base=%s)", LoginUser, bindAddr, absBase) + log.Printf("[SFTP] IP sans mot de passe autorisées: %v | OnlyAllowListedBeforeHandshake=%v", AllowedIPs, OnlyAllowListedBeforeHandshake) for { nConn, err := ln.Accept() @@ -172,6 +195,21 @@ func startSFTPServer(base string) { log.Printf("[SFTP] Accept err: %v", err) continue } + + remoteIP, _, _ := net.SplitHostPort(nConn.RemoteAddr().String()) + + // Optionnel : fermer immédiatement toute IP non allowlist + if OnlyAllowListedBeforeHandshake && !ipAllowed(nConn.RemoteAddr().String()) { + _ = nConn.Close() + continue + } + + // Rate limit anti-bruit + if !allow(remoteIP, 6, 12) { // ~6 requêtes/min, rafale 12 + _ = nConn.Close() + continue + } + if tc, ok := nConn.(*net.TCPConn); ok { _ = tc.SetKeepAlive(true) _ = tc.SetKeepAlivePeriod(15 * time.Second) @@ -180,7 +218,9 @@ func startSFTPServer(base string) { go func(conn net.Conn) { sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg) if err != nil { - log.Printf("[SFTP] Handshake err: %v", err) + if !isBenignHandshakeErr(err) { + log.Printf("[SFTP] Handshake warn: %v", err) + } _ = conn.Close() return } @@ -197,13 +237,15 @@ func startSFTPServer(base string) { log.Printf("[SFTP] Accept channel: %v", err) continue } + go func(in <-chan *ssh.Request) { for req := range in { switch req.Type { case "subsystem": // payload = uint32(len) + "sftp" if len(req.Payload) >= 4 && string(req.Payload[4:]) == "sftp" { - server, err := sftp.NewServer(ch) // API minimale + // NB: ceci ne "jaille" pas dans absBase ; pas de Chdir global non plus. + server, err := sftp.NewServer(ch) if err != nil { log.Printf("[SFTP] init error: %v", err) _, _ = ch.Stderr().Write([]byte("sftp init error\n")) @@ -211,7 +253,6 @@ func startSFTPServer(base string) { return } _ = req.Reply(true, nil) - if err := server.Serve(); err == io.EOF { _ = server.Close() } else if err != nil { @@ -264,7 +305,7 @@ func startHTTP() { func main() { // SFTP sur 2222 (root = ./upload) - go startSFTPServer(SFTPBaseDir) + go startSFTPServer(SFTPBaseDir, BindAddr) // HTTP normal startHTTP()