When I run a Node HTTP server app I usually call a custom function
function runAsWWW()
{
try
{
process.setgid('www-data');
process.setuid('www-data');
} catch (err)
{
console.error('Cowardly refusal to keep the process alive as root.');
process.exit(1);
}
}
from server.listen(8080,'localhost',null,runAsWWW);
so the server is actually running as the www-data user to offer a better modicum of security. Is there something similar I can do when I start up a Golang web server by issuing go run index.go?
No. You can't reliably setuid or setgid in go, because that doesn't work for multithreaded programs.
You need to start the program as the intended user, either directly, through a supervisor of some sort (e.g. supervisord, runit, monit), or through your init system.
Expanding on #JimB's answer:
Use a process supervisor to run your application as a specific user (and handle restarts/crashes, log re-direction, etc). setuid and setgid are universally bad ideas for multi-threaded applications.
Either use your OS' process manager (Upstart, systemd, sysvinit) or a standalone process manager (Supervisor, runit, monit, etc).
Here's an example for Supervisor:
[program:yourapp]
command=/home/yourappuser/bin/yourapp # the location of your app
autostart=true
autorestart=true
startretries=10
user=yourappuser # the user your app should run as (i.e. *not* root!)
directory=/srv/www/yourapp.com/ # where your application runs from
environment=APP_SETTINGS="/srv/www/yourapp.com/prod.toml" # environmental variables
redirect_stderr=true
stdout_logfile=/var/log/supervisor/yourapp.log # the name of the log file.
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
Further: if you're not reverse proxying and your Go application needs to bind to a port < 1024 (e.g. port 80 or 443) then use setcap - for example: setcap cap_net_bind_service=+ep /home/yourappuser/bin/yourapp
PS: I wrote a little article on how to run Go applications with Supervisor (starting from "I don't have Supervisor installed").
You can check if the program is running under a certain user with os/user package:
curr, err := user.Current()
// Check err.
www, err := user.Lookup("www-data")
// Check err.
if *curr != *www {
panic("Go away!")
}
This is not exactly what you want, but it does prevent it from running under any other user. You can run it as www-data by running it with su:
su www-data -c "myserver"
A way to achieve this safely would be to fork yourself.
This is a raw untested example on how you could achieve safe setuid:
1) Make sure you are root
2) Listen on the wanted port (as root)
3) Fork as www-data user.
4) Accept and serve requests.
http://play.golang.org/p/sT25P0KxXK
package main
import (
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/user"
"strconv"
"syscall"
)
var listenFD = flag.Int("l", 0, "listen pid")
func handler(w http.ResponseWriter, req *http.Request) {
u, err := user.Current()
if err != nil {
log.Println(err)
return
}
fmt.Fprintf(w, "%s\n", u.Name)
}
func lookupUser(username string) (uid, gid int, err error) {
u, err := user.Lookup(username)
if err != nil {
return -1, -1, err
}
uid, err = strconv.Atoi(u.Uid)
if err != nil {
return -1, -1, err
}
gid, err = strconv.Atoi(u.Gid)
if err != nil {
return -1, -1, err
}
return uid, gid, nil
}
// FDListener .
type FDListener struct {
file *os.File
}
// Accept .
func (ln *FDListener) Accept() (net.Conn, error) {
fd, _, err := syscall.Accept(int(*listenFD))
if err != nil {
return nil, err
}
conn, err := net.FileConn(os.NewFile(uintptr(fd), ""))
if err != nil {
return nil, err
}
return conn.(*net.TCPConn), nil
}
// Close .
func (ln *FDListener) Close() error {
return ln.file.Close()
}
// Addr .
func (ln *FDListener) Addr() net.Addr {
return nil
}
func start() error {
u, err := user.Current()
if err != nil {
return err
}
if u.Uid != "0" && *listenFD == 0 {
// we are not root and we have no listen fd. Error.
return fmt.Errorf("need to run as root: %s", u.Uid)
} else if u.Uid == "0" && *listenFD == 0 {
// we are root and we have no listen fd. Do the listen.
l, err := net.Listen("tcp", "0.0.0.0:80")
if err != nil {
return fmt.Errorf("Listen error: %s", err)
}
f, err := l.(*net.TCPListener).File()
if err != nil {
return err
}
uid, gid, err := lookupUser("guillaume")
if err != nil {
return err
}
// First extra file: fd == 3
cmd := exec.Command(os.Args[0], "-l", fmt.Sprint(3))
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("cmd.Run error: %s", err)
}
return nil
} else if u.Uid != "0" && *listenFD != 0 {
// We are not root and we have a listen fd. Do the accept.
ln := &FDListener{file: os.NewFile(uintptr(*listenFD), "net")}
if err := http.Serve(ln, http.HandlerFunc(handler)); err != nil {
return err
}
}
return fmt.Errorf("setuid fail: %s, %d", u.Uid, *listenFD)
}
func main() {
flag.Parse()
if err := start(); err != nil {
log.Fatal(err)
}
}
Related
Short description of the setup:
I have a go executable running as a linux service on a compute engine instance on GCP.
This go program should start a other executable in a cluster with mpirun but the command always fails with code 1.
No its getting wired, I print out the command before I run it. If I trigger the command manually via ssh on the server it works flawlessly. So mpi seems to be setup correctly.
func (r *EngineRunner) clusterCommand(executable string) (string, []string) {
slots := len(r.nodeManager.Nodes) + 1
return "mpirun", []string{
"--oversubscribe",
"--hostfile",
constants.GetMPIHostFilePath(),
"-np",
strconv.Itoa(slots),
executable,
}
}
// OpenStockfishSession returns already open session or creates a new one. If a new one is created second param is true.
func (r *EngineRunner) OpenStockfishSession(mode string, session string) (*EngineSession, bool, error) {
r.sessionLock.Lock()
defer r.sessionLock.Unlock()
fullSessionKey := EngineNameStockfish + "/" + mode + "/" + session
if s, ok := r.sessions[fullSessionKey]; ok {
return s, false, nil
}
var cmd *exec.Cmd
if mode == ModeMaster {
println("run: " + GetStockfishBinaryPath())
cmd = exec.Command(GetStockfishBinaryPath())
} else if mode == ModeCluster {
com, args := r.clusterCommand(GetStockfishBinaryPath())
println("run: " + com + " " + strings.Join(args, " "))
cmd = exec.Command(com, args...)
} else {
return nil, false, errors.New("invalid mode")
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, false, err
}
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, false, err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, false, err
}
// Start the command
if err := cmd.Start(); err != nil {
return nil, false, err
}
newSession := &EngineSession{
cmd: cmd,
StdIn: stdin,
StdOut: stdout,
}
r.sessions[fullSessionKey] = newSession
go func() {
defer func() {
delete(r.sessions, fullSessionKey)
}()
if err := cmd.Wait(); err != nil {
println("error while waiting for stockfish process", err.Error())
stderrStr, _ := io.ReadAll(stderr)
stdoutStr, _ := io.ReadAll(stdout)
println("Stderr: " + string(stderrStr))
println("Stdout: " + string(stdoutStr))
}
}()
return newSession, true, nil
}
The printed command looks like this:
mpirun --oversubscribe --hostfile /usr/sbin/cec_rd/mpi_hostfile -np 3 /usr/sbin/cec_rd/engines/stockfish/stockfish
The paths seem to be correct and also mpi is setup correctly, as if I copy this command and run it via ssh it works...
Hope someone has an idea as I am kind of done, cheers 🥂
What I need is to perform the equivalent of the following command but in Go code:
ssh -L 9999:192.168.1.1:80 -J root#[IPv6 address] myuser#100.1.1.100
I'm not even sure where to start with this one.
I haven't been able to find any examples online and I'm at a loss.
Does anyone know how this could be done in Go?
package main
import (
"io"
"log"
"net"
"golang.org/x/crypto/ssh"
)
func main() {
client, err := ssh.Dial("tcp", "100.1.1.100:22", &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{ssh.Password("")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
log.Panicln(err)
return
}
log.Println("init ssh client")
ln, err := net.Listen("tcp", ":9999")
if err != nil {
log.Panicln(err)
return
}
log.Println("local listen")
for {
localconn, err := ln.Accept()
if err != nil {
log.Panicln(err)
return
}
sshconn, err := client.DialTCP("", nil, &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 80})
if err != nil {
log.Panicln(err)
return
}
// local <--> remote
go func() {
errc := make(chan error, 1)
spc := switchProtocolCopier{user: localconn, backend: sshconn}
go spc.copyToBackend(errc)
go spc.copyFromBackend(errc)
log.Printf("stop conn error: %v\n", <-errc)
}()
}
}
// switchProtocolCopier exists so goroutines proxying data back and
// forth have nice names in stacks.
type switchProtocolCopier struct {
user, backend io.ReadWriter
}
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
_, err := io.Copy(c.user, c.backend)
errc <- err
}
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
_, err := io.Copy(c.backend, c.user)
errc <- err
}
go1.6 File method WriteStringfrequent calls led to a large system cache.
How to solve this problem.
go env: linux amd64.
Is this a problem of Linux system?
code:
package main
import (
"fmt"
"net/http"
"os"
"time"
)
var logCtxCh chan *http.Request
var accessLogFile *os.File
type HandlerHttp struct{}
func (this *HandlerHttp) ServeHTTP(w http.ResponseWriter, req *http.Request) {
sendAccessLog(req)
w.Write([]byte("Hello Word"))
}
func main() {
s := &http.Server{
Addr: ":8012",
Handler: &HandlerHttp{},
}
logCtxCh = make(chan *http.Request, 500)
go startAcessLog()
err:= s.ListenAndServe()
fmt.Println(err.Error())
}
func startAcessLog() {
for {
select {
case ctx := <-logCtxCh:
handleAccessLog(ctx)
}
}
}
func sendAccessLog(req *http.Request) {
logCtxCh <- req
}
func handleAccessLog(req *http.Request) {
uri := req.RequestURI
ip := req.RemoteAddr
agent := req.UserAgent()
refer := req.Referer()
method := req.Method
now := time.Now().Format("2006-01-02 15:04:05")
logText := fmt.Sprintf("%s %s %s %s %s %s\n",
now,
ip,
method,
uri,
agent,
refer,
)
fileName := fmt.Sprintf("/data/logs/zyapi/access_zyapi%s.log",
time.Now().Format("2006010215"),
)
writeLog(fileName, logText)
}
func writeLog(fileName, logText string) {
var err error
var exist = true
if _, err = os.Stat(fileName); os.IsNotExist(err) {
exist = false
}
if exist == false {
if accessLogFile != nil {
accessLogFile.Sync()
accessLogFile.Close()
}
accessLogFile, err = os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err == nil {
_, err = accessLogFile.WriteString(logText)
}
if err != nil {
fmt.Errorf(err.Error())
}
} else {
if accessLogFile == nil {
accessLogFile, err = os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Errorf(err.Error())
return
}
}
_, err = accessLogFile.WriteString(logText)
if err != nil {
fmt.Errorf(err.Error())
}
}
}
test:
ab -n100000 -c10 -k "http://127.0.0.1:8012/"
ab -n100000 -c10 -k "http://127.0.0.1:8012/"
ab -n100000 -c10 -k "http://127.0.0.1:8012/"
ab -n100000 -c10 -k "http://127.0.0.1:8012/"
ab -n100000 -c10 -k "http://127.0.0.1:8012/"
After running several times the system file cache becomes very large
CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O BLOCK I/O
api_8011 38.47% 6.442GB/6.442GB 100.00% 0B/0B 0B/115.4MB
api_8012 36.90% 6.442GB/6.442GB 99.99% 0B/0B 0B/115.6 MB
There's a bunch of things going on, I can't spot the bug right away but these things will help:
Try to use bufio.Writer as much as possible if you are calling file.WriteString, otherwise every single write will be a syscall, hurting performance.
You don't need to use select in you startAccessLog function:
func startAcessLog() {
for ctx := <-logCtxCh {
handleAccessLog(ctx)
}
}
Change your error checks from:
if err != nil {
fmt.Errorf(err.Error())
}
to:
if err != nil {
fmt.Println(err)
}
otherwise you are not printing errors. fmt.Errorf formats a string, like fmt.Sprintf does and returns it as an error. It doesn't print anything at all.
You should guard accessLog with a sync.Mutex or write to it via a channel. Why? Because there's more than one goroutine trying to work with accessLog and you don't want data races to happen.
Doing it via a channel would simplify your writeLog function a log. Currently it's hard to follow the logic. I initially thought you weren't properly closing the file.
I setup a webserver and I use my own package where I do some write/read from and to files. When the server gets a tcp connection, I start a different goroutine to handle the request for each connection. In the request handler func, I call the func DoSomething() of some_package.
Here's the code for web_server.go:
package main
import (
sp "./some_package"
"log"
"net"
"os"
"net/http"
)
func main() {
l, err := net.Listen("tcp", "0.0.0.0" + ":" + "4567")
if err != nil {
log.Println("Error listening:", err.Error())
os.Exit(1)
}
defer l.Close()
log.Println("Listening on 0.0.0.0:4567")
go func() {
for {
// Listen for an incoming connection.
conn, err := l.Accept()
if err != nil {
log.Println("Error accepting: ", err.Error())
os.Exit(1)
}
// Handle connections in a new goroutine.
go handlerFunction(conn)
}
}()
log.Printf("Setting up the Webserver...")
err = http.ListenAndServe("0.0.0.0:"+"4568", nil)
if err != nil {
log.Fatal(err)
}
}
func handlerFunction(conn net.Conn) {
defer conn.Close()
sp.DoSomething()
}
The function DoSomething() reads and writes to file. You can see the code where it is declared in the package:
package some_package
import (
"io/ioutil"
"strconv"
"os"
"log"
)
func IncrementValue(pastValue string)(newValue string){
newValueInt, _ := strconv.Atoi(pastValue)
return strconv.Itoa(newValueInt + 1)
}
func DoSomething() (err error){
initialValue := "1"
filename := "myFile.txt"
if _, err := os.Stat(filename); err == nil {
someText, err := ioutil.ReadFile(filename)
if err != nil {
log.Printf("Error reading")
return err
}
newValue := IncrementValue(string(someText))
err = ioutil.WriteFile(filename,[]byte(newValue), 0644)
if err != nil {
return err
}
}else{
err = ioutil.WriteFile(filename,[]byte(initialValue), 0644)
if err != nil {
return err
}
}
return
}
How can I use a locking mechanism like mutex.Lock and mutex.Unlock in this case to make the reading and writing to file concurrent so when one routine which is currently writing can stop the other from reading till the first one writes to file successfully?
Is my example suitable to be concurrent when reading or writing to file?
Is this the right approach to do so? Thank You
You can't make the reading and writing of a file concurrent (well, it's possible, but not with the access pattern you're describing). Use a single mutex to serialize all access to your file:
var fileMutex sync.Mutex
func DoSomething() {
fileMutex.Lock()
defer fileMutex.Unlock()
//...
}
I need to use password authenticated scp to download a file from a server. How do I do so using Go? Tried the following code, but it doesn't pass in the password.
package main
import (
"os/exec"
"time"
)
func main() {
password := "password"
cmd := exec.Command("scp", "admin#192.168.1.150:file", "file")
in, err := cmd.StdinPipe()
if err != nil {
panic(err)
}
defer in.Close()
out, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
defer out.Close()
if err = cmd.Run(); err != nil {
panic(err)
}
go func() {
time.Sleep(10 * time.Second)
_, err = in.Write([]byte(password + "\n"))
if err != nil {
panic(err)
}
}()
}
Edit: I ended up using the gexpect (github.com/ThomasRooney/gexpect) library.
package main
import (
"github.com/ThomasRooney/gexpect"
"log"
)
func main() {
child, err := gexpect.Spawn("scp admin#192.168.1.150:file file")
if err != nil {
log.Fatalln(err)
}
child.Expect("password:")
child.SendLine("password")
child.Interact()
child.Close()
}
The answer to this self-answered question might help:
Golang write input and get output from terminal process
at least, he mentions in the answer that he "was able to get ssh access working with a password", which is not mentioned explicitly in the question - that's why you probably didn't find it while searching the site?