up
This commit is contained in:
parent
49d97937fe
commit
46d078dde1
265
main.go
265
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()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user