package app import ( "fmt" "io" "log" "net" "net/http" "strings" "sync" "time" "infinite-noodle/internal/noodle" "infinite-noodle/internal/web" ) type Config struct { DataPath string Host string Port int RunTest bool } func Run(cfg Config) error { listenAddr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) noodleChannel := make(chan noodle.Noodle) db := noodle.NewDatabase(cfg.DataPath) defer db.Close() auth, err := web.NewAuth(db) if err != nil { return err } if err := auth.EnsureDefaultUser(); err != nil { return err } if cfg.RunTest { runTestSequence(db) } go systemCheck(db, noodleChannel) go expirationCheck(db, noodleChannel) go proxify(db, noodleChannel) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(web.StaticFiles())))) http.HandleFunc("/login", auth.HandleLogin()) http.HandleFunc("/logout", auth.HandleLogout()) http.HandleFunc("/", auth.RequireAuth(web.HandleMain(db, &noodleChannel, auth))) http.HandleFunc("/users", auth.RequireAuth(web.HandleUsers(auth, db))) http.HandleFunc("/users/add", auth.RequireAdmin(web.HandleAddUser(auth, db))) http.HandleFunc("/users/role", auth.RequireAdmin(web.HandleSetUserRole(auth, db))) http.HandleFunc("/users/password", auth.RequireAuth(web.HandleChangePassword(auth, db))) http.HandleFunc("/users/delete", auth.RequireAuth(web.HandleDeleteUser(auth, db))) http.HandleFunc("/add", auth.RequireAuth(web.HandleAdd(db, &noodleChannel, auth))) http.HandleFunc("/toggle", auth.RequireAuth(web.HandleToggle(db, &noodleChannel))) http.HandleFunc("/delete", auth.RequireAuth(web.HandleDelete(db, &noodleChannel))) log.Printf("Server starting on %s", listenAddr) return http.ListenAndServe(listenAddr, nil) } func systemCheck(db *noodle.Database, noodleChannel chan noodle.Noodle) { for { noodles := db.GetAll() for _, item := range noodles { noodleChannel <- item } time.Sleep(60 * time.Second) } } func proxify(db *noodle.Database, noodleChannel chan noodle.Noodle) { noodleMap := make(map[string]managedProxy) for { item := <-noodleChannel _, running := noodleMap[item.Id] if item.IsUp && !running { proxy, err := newProxy(item) if err != nil { log.Print(err) if _, updateErr := db.SetIsUp(item.Id, false); updateErr != nil { log.Print(updateErr) } continue } noodleMap[item.Id] = proxy go proxy.Run() continue } if !item.IsUp && running { log.Printf("Closing noodle=%v", item) if err := noodleMap[item.Id].Close(); err != nil { log.Print(err) } delete(noodleMap, item.Id) } } } type managedProxy interface { Run() Close() error } func newProxy(item noodle.Noodle) (managedProxy, error) { listenAddr := fmt.Sprintf("0.0.0.0:%d", item.ListenPort) targetAddr := fmt.Sprintf("%s:%d", item.DestHost, item.DestPort) proto := normalizeProto(item.Proto) log.Printf("Starting a %s noodle from %s to %s with source=%s", proto, listenAddr, targetAddr, item.Src) switch proto { case "UDP": return newUDPNoodleProxy(listenAddr, targetAddr, item.Src) default: return newTCPNoodleProxy(listenAddr, targetAddr, item.Src) } } func normalizeProto(proto string) string { switch strings.ToUpper(strings.TrimSpace(proto)) { case "UDP": return "UDP" default: return "TCP" } } type tcpNoodleProxy struct { listenAddr string targetAddr string allowedIP string listener net.Listener mu sync.Mutex conns map[net.Conn]struct{} } func newTCPNoodleProxy(listenAddr, targetAddr, allowedIP string) (*tcpNoodleProxy, error) { ln, err := net.Listen("tcp", listenAddr) if err != nil { return nil, err } return &tcpNoodleProxy{ listenAddr: listenAddr, targetAddr: targetAddr, allowedIP: allowedIP, listener: ln, conns: make(map[net.Conn]struct{}), }, nil } func (p *tcpNoodleProxy) Run() { for { conn, err := p.listener.Accept() if err != nil { if isClosedNetworkError(err) { return } log.Printf("Accept failed for %s: %v", p.listenAddr, err) return } go p.handleConn(conn) } } func (p *tcpNoodleProxy) handleConn(src net.Conn) { if !allowSource(p.allowedIP, src.RemoteAddr()) { log.Printf("Rejected noodle connection from %s; allowed source is %s", src.RemoteAddr().String(), p.allowedIP) src.Close() return } dst, err := net.Dial("tcp", p.targetAddr) if err != nil { log.Printf("Dial failed from %s to %s: %v", p.listenAddr, p.targetAddr, err) src.Close() return } p.trackConn(src) p.trackConn(dst) defer p.untrackAndClose(src) defer p.untrackAndClose(dst) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() if _, err := io.Copy(dst, src); err != nil && !isClosedNetworkError(err) { log.Printf("Proxy copy src->dst failed for %s to %s: %v", p.listenAddr, p.targetAddr, err) } closeWrite(dst) }() go func() { defer wg.Done() if _, err := io.Copy(src, dst); err != nil && !isClosedNetworkError(err) { log.Printf("Proxy copy dst->src failed for %s to %s: %v", p.targetAddr, p.listenAddr, err) } closeWrite(src) }() wg.Wait() } func (p *tcpNoodleProxy) Close() error { err := p.listener.Close() p.mu.Lock() defer p.mu.Unlock() for conn := range p.conns { _ = conn.Close() } return err } func (p *tcpNoodleProxy) trackConn(conn net.Conn) { p.mu.Lock() defer p.mu.Unlock() p.conns[conn] = struct{}{} } func (p *tcpNoodleProxy) untrackAndClose(conn net.Conn) { p.mu.Lock() delete(p.conns, conn) p.mu.Unlock() _ = conn.Close() } type udpNoodleProxy struct { listenAddr string targetAddr string allowedIP string conn *net.UDPConn mu sync.Mutex sessions map[string]*udpSession closed bool } type udpSession struct { clientAddr *net.UDPAddr backend *net.UDPConn lastSeen time.Time } const udpSessionIdleTimeout = 2 * time.Minute func newUDPNoodleProxy(listenAddr, targetAddr, allowedIP string) (*udpNoodleProxy, error) { addr, err := net.ResolveUDPAddr("udp", listenAddr) if err != nil { return nil, err } conn, err := net.ListenUDP("udp", addr) if err != nil { return nil, err } return &udpNoodleProxy{ listenAddr: listenAddr, targetAddr: targetAddr, allowedIP: allowedIP, conn: conn, sessions: make(map[string]*udpSession), }, nil } func (p *udpNoodleProxy) Run() { go p.cleanupIdleSessions() buf := make([]byte, 65535) for { n, clientAddr, err := p.conn.ReadFromUDP(buf) if err != nil { if isClosedNetworkError(err) { return } log.Printf("UDP read failed for %s: %v", p.listenAddr, err) return } if !allowSource(p.allowedIP, clientAddr) { log.Printf("Rejected noodle datagram from %s; allowed source is %s", clientAddr.String(), p.allowedIP) continue } session, err := p.sessionFor(clientAddr) if err != nil { log.Printf("UDP session setup failed for %s to %s: %v", clientAddr.String(), p.targetAddr, err) continue } payload := append([]byte(nil), buf[:n]...) if _, err := session.backend.Write(payload); err != nil && !isClosedNetworkError(err) { log.Printf("UDP write failed from %s to %s: %v", clientAddr.String(), p.targetAddr, err) p.removeSession(clientAddr.String(), true) } } } func (p *udpNoodleProxy) sessionFor(clientAddr *net.UDPAddr) (*udpSession, error) { key := clientAddr.String() p.mu.Lock() if session, ok := p.sessions[key]; ok { session.lastSeen = time.Now() p.mu.Unlock() return session, nil } p.mu.Unlock() targetAddr, err := net.ResolveUDPAddr("udp", p.targetAddr) if err != nil { return nil, err } backend, err := net.DialUDP("udp", nil, targetAddr) if err != nil { return nil, err } session := &udpSession{ clientAddr: clientAddr, backend: backend, lastSeen: time.Now(), } p.mu.Lock() if existing, ok := p.sessions[key]; ok { p.mu.Unlock() _ = backend.Close() return existing, nil } p.sessions[key] = session p.mu.Unlock() go p.relayBackend(session) return session, nil } func (p *udpNoodleProxy) relayBackend(session *udpSession) { buf := make([]byte, 65535) for { n, err := session.backend.Read(buf) if err != nil { if !isClosedNetworkError(err) { log.Printf("UDP backend read failed for %s: %v", p.targetAddr, err) } p.removeSession(session.clientAddr.String(), false) return } if _, err := p.conn.WriteToUDP(buf[:n], session.clientAddr); err != nil { if !isClosedNetworkError(err) { log.Printf("UDP client write failed for %s: %v", session.clientAddr.String(), err) } p.removeSession(session.clientAddr.String(), true) return } p.mu.Lock() if current, ok := p.sessions[session.clientAddr.String()]; ok { current.lastSeen = time.Now() } p.mu.Unlock() } } func (p *udpNoodleProxy) cleanupIdleSessions() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { if p.isClosed() { return } cutoff := time.Now().Add(-udpSessionIdleTimeout) var expired []string p.mu.Lock() for key, session := range p.sessions { if session.lastSeen.Before(cutoff) { expired = append(expired, key) } } p.mu.Unlock() for _, key := range expired { p.removeSession(key, true) } } } func (p *udpNoodleProxy) removeSession(key string, closeBackend bool) { p.mu.Lock() session, ok := p.sessions[key] if ok { delete(p.sessions, key) } p.mu.Unlock() if ok && closeBackend { _ = session.backend.Close() } } func (p *udpNoodleProxy) isClosed() bool { p.mu.Lock() defer p.mu.Unlock() return p.closed } func (p *udpNoodleProxy) Close() error { p.mu.Lock() if p.closed { p.mu.Unlock() return nil } p.closed = true sessions := p.sessions p.sessions = make(map[string]*udpSession) conn := p.conn p.mu.Unlock() for _, session := range sessions { _ = session.backend.Close() } if conn == nil { return nil } return conn.Close() } func allowSource(allowedIPs string, addr net.Addr) bool { if allowedIPs == "" || strings.EqualFold(strings.TrimSpace(allowedIPs), "All") { return true } host, _, err := net.SplitHostPort(addr.String()) if err != nil { return false } hostIP := normalizeIP(net.ParseIP(host)) if hostIP == nil { return false } for _, item := range strings.Split(allowedIPs, ",") { entry := strings.TrimSpace(item) if strings.Contains(entry, "/") { _, network, err := net.ParseCIDR(entry) if err == nil && networkContainsIP(network, hostIP) { return true } continue } entryIP := normalizeIP(net.ParseIP(entry)) if entryIP != nil && entryIP.Equal(hostIP) { return true } } return false } func normalizeIP(ip net.IP) net.IP { if ip == nil { return nil } if v4 := ip.To4(); v4 != nil { return v4 } return ip } func networkContainsIP(network *net.IPNet, ip net.IP) bool { if network == nil || ip == nil { return false } if network.Contains(ip) { return true } networkIP := normalizeIP(network.IP) normalizedIP := normalizeIP(ip) if networkIP == nil || normalizedIP == nil { return false } if len(networkIP) != len(normalizedIP) { return false } mask := network.Mask if len(mask) != len(normalizedIP) { return false } for i := range normalizedIP { if normalizedIP[i]&mask[i] != networkIP[i]&mask[i] { return false } } return true } func closeWrite(conn net.Conn) { if tcpConn, ok := conn.(*net.TCPConn); ok { _ = tcpConn.CloseWrite() } } func isClosedNetworkError(err error) bool { if err == nil { return false } if net.ErrClosed != nil && err == net.ErrClosed { return true } return strings.Contains(err.Error(), "use of closed network connection") } func expirationCheck(db *noodle.Database, noodleChannel chan noodle.Noodle) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for range ticker.C { stopped := db.TickExpirations() for _, item := range stopped { noodleChannel <- item } } } func runTestSequence(db *noodle.Database) { for i := 0; i < 21; i++ { item := noodle.Noodle{ Id: db.MakeID(), Name: "Name_Test", Proto: "TCP", Src: "All", ListenPort: 1080 + i, DestPort: 22, DestHost: "localhost", Expiration: time.Duration(time.Now().Second()) * time.Second, IsUp: true, } log.Printf("Test noodle=%v", item) db.Add(item) log.Printf("Test id=%s exists=%t", item.Id, db.Handle.Has(item.Id)) log.Printf("Test GetAllGeneric=%v", db.GetAllGeneric()) } }