545 lines
12 KiB
Go
545 lines
12 KiB
Go
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())
|
|
}
|
|
}
|