diff --git a/api/gosrc/go.mod b/api/gosrc/go.mod index 8f40ef1..1df32b6 100644 --- a/api/gosrc/go.mod +++ b/api/gosrc/go.mod @@ -13,6 +13,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/crypto v0.29.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.27.0 // indirect ) diff --git a/api/gosrc/go.sum b/api/gosrc/go.sum index 8cc0806..c6ca623 100644 --- a/api/gosrc/go.sum +++ b/api/gosrc/go.sum @@ -21,8 +21,12 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/api/gosrc/sshbackend/main.go b/api/gosrc/sshbackend/main.go new file mode 100644 index 0000000..18916e4 --- /dev/null +++ b/api/gosrc/sshbackend/main.go @@ -0,0 +1,213 @@ +package main + +import ( + "encoding/json" + "fmt" + "net" + "os" + "strings" + + "git.greysoh.dev/imterah/nextnet/backendutil" + "git.greysoh.dev/imterah/nextnet/commonbackend" + "github.com/charmbracelet/log" + "golang.org/x/crypto/ssh" +) + +type SSHBackend struct { + data SSHBackendData + conn ssh.Client + clients []*commonbackend.ClientConnection +} + +type SSHBackendData struct { + Ip string `json:"ip"` + Port uint16 `json:"port"` + Username string `json:"username"` + PrivateKey string `json:"privateKey"` + ListenOnIPs []string `json:"listenOnIPs"` +} + +func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { + var backendData SSHBackendData + + err := json.Unmarshal(bytes, &backendData) // ????? + if err != nil { + return false, err + } + backend.data = backendData + + if len(backend.data.ListenOnIPs) == 0 { + backend.data.ListenOnIPs = []string{"0.0.0.0"} + } + + // create signer for privateKey + signer, err := ssh.ParsePrivateKey([]byte(backendData.PrivateKey)) + if err != nil { + return false, err + } + + auth := ssh.PublicKeys(signer) + + config := &ssh.ClientConfig{ + User: backendData.Username, + Auth: []ssh.AuthMethod{ + auth, + }, + } + + conn, err := ssh.Dial("tcp", backendData.Ip+":"+string(backendData.Port), config) + if err != nil { + return false, err + } + backend.conn = conn + + return true, nil +} + +func (backend *SSHBackend) StopBackend() (bool, error) { + err := backend.conn.Close() + if err != nil { + return false, err + } + + return true, nil +} + +func (backend *SSHBackend) AddConnection(command *commonbackend.AddConnectionCommand) (bool, error) { + for _, ipListener := range backend.data.ListenOnIPs { + ip := net.TCPAddr{ + IP: net.ParseIP(ipListener), + Port: int(command.DestPort), + } + listener, err := backend.conn.ListenTCP(&ip) + if err != nil { + return false, err + } + go func() { + for { + forwardedConn, err := listener.Accept() + if err != nil { + log.Warnf("failed to accept listener connection: %s", err.Error()) + continue + } + sourceConn, err := net.Dial("tcp", command.SourceIP+":"+string(command.SourcePort)) + if err != nil { + log.Warnf("failed to dial source connection: %s", err.Error()) + continue + } + sourceBuffer := make([]byte, 65535) + forwardedBuffer := make([]byte, 65535) + go func() { + defer sourceConn.Close() + defer forwardedConn.Close() + + for { + len, err := forwardedConn.Read(forwardedBuffer) + if err != nil { + log.Errorf("failed to read from forwarded connection: %s", err.Error()) + return + } + + _, err = sourceConn.Write(forwardedBuffer[:len]) + if err != nil { + log.Errorf("failed to write to source connection: %s", err.Error()) + return + } + } + }() + go func() { + defer sourceConn.Close() + defer forwardedConn.Close() + + for { + len, err := sourceConn.Read(sourceBuffer) + if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + log.Errorf("failed to read from source connection: %s", err.Error()) + return + } + + _, err = forwardedConn.Write(sourceBuffer[:len]) + if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + log.Errorf("failed to write to forwarded connection: %s", err.Error()) + return + } + } + }() + } + }() + } + + return true, nil +} + +func (backend *SSHBackend) RemoveConnection(command *commonbackend.RemoveConnectionCommand) (bool, error) { + // FIXME: implement + return true, nil +} + +func (backend *SSHBackend) GetAllConnections() []*commonbackend.ClientConnection { + // return []*commonbackend.ClientConnection{} + return backend.clients +} + +func (backend *SSHBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { + if clientParameters.Protocol != "tcp" { + return &commonbackend.CheckParametersResponse{ + IsValid: false, + Message: "Only TCP is supported", + } + } + + return &commonbackend.CheckParametersResponse{ + IsValid: true, + } +} + +func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { + var backendData SSHBackendData + + err := json.Unmarshal(arguments, &backendData) // ????? + if err != nil { + return &commonbackend.CheckParametersResponse{ + IsValid: false, + Message: fmt.Sprintf("could not read json: %s", err.Error()), + } + } + + return &commonbackend.CheckParametersResponse{ + IsValid: true, + } +} + +func main() { + // When using logging, you should use charmbracelet/log, because that's what everything else uses in this ecosystem of a project. - imterah + logLevel := os.Getenv("NEXTNET_LOG_LEVEL") + + if logLevel != "" { + switch logLevel { + case "debug": + log.SetLevel(log.DebugLevel) + + case "info": + log.SetLevel(log.InfoLevel) + + case "warn": + log.SetLevel(log.WarnLevel) + + case "error": + log.SetLevel(log.ErrorLevel) + + case "fatal": + log.SetLevel(log.FatalLevel) + } + } + + backend := &SSHBackend{} + + application := backendutil.NewHelper(backend) + err := application.Start() + + if err != nil { + log.Fatalf("failed execution in application: %s", err.Error()) + } +}