Skip to content

Commit 21a813a

Browse files
authored
Allow adding VIP if the user has sudo permission (#629)
1 parent 4f4744a commit 21a813a

File tree

5 files changed

+111
-10
lines changed

5 files changed

+111
-10
lines changed

.golangci.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ issues:
2626
linters:
2727
- gosec
2828
text: "G101:"
29+
- path: pkg/util/cmd/cmd.go
30+
linters:
31+
- gosec
32+
text: "G204:"
2933

3034
linters:
3135
enable:

pkg/manager/vip/network.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ package vip
55

66
import (
77
"runtime"
8+
"syscall"
89

910
"github.com/j-keck/arping"
1011
"github.com/pingcap/tiproxy/lib/util/errors"
12+
"github.com/pingcap/tiproxy/pkg/util/cmd"
1113
"github.com/vishvananda/netlink"
1214
)
1315

@@ -68,13 +70,26 @@ func (no *networkOperation) HasIP() (bool, error) {
6870
}
6971

7072
func (no *networkOperation) AddIP() error {
71-
return netlink.AddrAdd(no.link, no.address)
73+
err := netlink.AddrAdd(no.link, no.address)
74+
// If TiProxy is deployed by TiUP, the user that runs TiProxy only has the sudo permission.
75+
if err != nil && errors.Is(err, syscall.EPERM) {
76+
err = cmd.ExecCmd("sudo", "ip", "addr", "add", no.address.String(), "dev", no.link.Attrs().Name)
77+
}
78+
return errors.WithStack(err)
7279
}
7380

7481
func (no *networkOperation) DeleteIP() error {
75-
return netlink.AddrDel(no.link, no.address)
82+
err := netlink.AddrDel(no.link, no.address)
83+
if err != nil && errors.Is(err, syscall.EPERM) {
84+
err = cmd.ExecCmd("sudo", "ip", "addr", "del", no.address.String(), "dev", no.link.Attrs().Name)
85+
}
86+
return errors.WithStack(err)
7687
}
7788

7889
func (no *networkOperation) SendARP() error {
79-
return arping.GratuitousArpOverIfaceByName(no.address.IP, no.link.Attrs().Name)
90+
err := arping.GratuitousArpOverIfaceByName(no.address.IP, no.link.Attrs().Name)
91+
if err != nil && errors.Is(err, syscall.EPERM) {
92+
err = cmd.ExecCmd("sudo", "arping", "-c", "1", "-U", "-I", no.link.Attrs().Name, no.address.IP.String())
93+
}
94+
return errors.WithStack(err)
8095
}

pkg/manager/vip/network_test.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ func TestAddDelIP(t *testing.T) {
5353
require.NotNil(t, operation, "case %d", i)
5454

5555
err = operation.AddIP()
56-
// Maybe the privilege is not granted.
57-
if err != nil && strings.Contains(err.Error(), "operation not permitted") {
56+
// Maybe the command is not installed.
57+
if err != nil && strings.Contains(err.Error(), "command not found") {
5858
continue
5959
}
6060
if test.addErr != "" {
@@ -65,11 +65,13 @@ func TestAddDelIP(t *testing.T) {
6565
}
6666

6767
err = operation.SendARP()
68-
if test.sendErr != "" {
69-
require.Error(t, err, "case %d", i)
70-
require.Contains(t, err.Error(), test.sendErr, "case %d", i)
71-
} else {
72-
require.NoError(t, err, "case %d", i)
68+
if err == nil || !strings.Contains(err.Error(), "command not found") {
69+
if test.sendErr != "" {
70+
require.Error(t, err, "case %d", i)
71+
require.Contains(t, err.Error(), test.sendErr, "case %d", i)
72+
} else {
73+
require.NoError(t, err, "case %d", i)
74+
}
7375
}
7476

7577
if err := operation.DeleteIP(); test.delErr != "" {

pkg/util/cmd/cmd.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2024 PingCAP, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
"os/exec"
9+
"strings"
10+
11+
"github.com/pingcap/tiproxy/lib/util/errors"
12+
)
13+
14+
// ExecCmd executes commands with checking potential tainted input.
15+
func ExecCmd(cmd string, args ...string) error {
16+
cmds := make([]string, 0, len(args)+1)
17+
if !isValidArg(cmd) {
18+
return errors.Errorf("invalid cmd: %s", cmd)
19+
}
20+
cmds = append(cmds, cmd)
21+
for _, arg := range args {
22+
if !isValidArg(arg) {
23+
return errors.Errorf("invalid argument: %s", arg)
24+
}
25+
cmds = append(cmds, escapeArg(arg))
26+
}
27+
output, err := exec.Command(cmds[0], cmds[1:]...).CombinedOutput()
28+
if err != nil {
29+
return errors.Wrapf(errors.WithStack(err), "output: %s", string(output))
30+
}
31+
return nil
32+
}
33+
34+
func isValidArg(arg string) bool {
35+
dangerousChars := []string{";", "&", "|", "`", "$(", "${", "<", ">", ">>"}
36+
for _, char := range dangerousChars {
37+
if strings.Contains(arg, char) {
38+
return false
39+
}
40+
}
41+
return true
42+
}
43+
44+
func escapeArg(arg string) string {
45+
return fmt.Sprintf("'%s'", strings.ReplaceAll(arg, "'", "'\\''"))
46+
}

pkg/util/cmd/cmd_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2024 PingCAP, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestExecCmd(t *testing.T) {
13+
tests := []struct {
14+
cmds []string
15+
hasErr bool
16+
}{
17+
{
18+
cmds: []string{"echo", "$(whoami)"},
19+
hasErr: true,
20+
},
21+
{
22+
cmds: []string{"echo", "abc"},
23+
},
24+
{
25+
cmds: []string{"hello"},
26+
hasErr: true,
27+
},
28+
}
29+
30+
for i, test := range tests {
31+
err := ExecCmd(test.cmds[0], test.cmds[1:]...)
32+
require.Equal(t, test.hasErr, err != nil, "case %d", i)
33+
}
34+
}

0 commit comments

Comments
 (0)