From d9b9516ed6a372b1270011c2177681a9713c9d31 Mon Sep 17 00:00:00 2001 From: Mikhail Osipov Date: Wed, 22 Sep 2021 02:51:33 +0300 Subject: auth: via file --- pkg/netstring/netstring.go | 22 ++++++ pkg/server/hook/auth.go | 188 ++++++++++++++++++++++++++++++++------------ pkg/server/hook/proxy.go | 2 +- pkg/server/socket/socket.go | 4 +- pkg/test/auth_test.go | 68 +++++++++++++++- pkg/test/defer_test.go | 2 +- pkg/test/exec_test.go | 2 +- pkg/test/hook_test.go | 4 +- pkg/test/proxy_test.go | 4 +- 9 files changed, 234 insertions(+), 62 deletions(-) (limited to 'pkg') diff --git a/pkg/netstring/netstring.go b/pkg/netstring/netstring.go index 62507f9..00722a8 100644 --- a/pkg/netstring/netstring.go +++ b/pkg/netstring/netstring.go @@ -59,3 +59,25 @@ func (d *Decoder) Decode() (out string, err error) { return string(buf[:n]), nil } + +func Encode(w io.Writer, args ...string) error { + e := Encoder{w: w} + for _, s := range args { + if err := e.Encode(s); err != nil { + return err + } + } + return nil +} + +func Decode(r io.Reader, args ...*string) error { + d := Decoder{r: r} + for _, s := range args { + t, err := d.Decode() + if err != nil { + return err + } + *s = t + } + return nil +} diff --git a/pkg/server/hook/auth.go b/pkg/server/hook/auth.go index 21f900e..0f70d88 100644 --- a/pkg/server/hook/auth.go +++ b/pkg/server/hook/auth.go @@ -1,11 +1,14 @@ package hook import ( + "bufio" "crypto/md5" "crypto/rand" "errors" + "fmt" "io" - "sync" + "os" + "strings" "time" "tunnel/pkg/server/env" @@ -17,62 +20,109 @@ const authTimeout = 5 * time.Second const saltSize = 16 const hashSize = md5.Size +var hashNone = strings.Repeat("\x00", hashSize) + type authHook struct { - m sync.Map + Passive bool } type auth struct { - h *authHook - - secret string - - salt struct { - self string - peer string + file *os.File + user string + salt string + pass string + + peer struct { + user string + salt string } hash string - readSaltDone chan struct{} - readHashDone chan struct{} + passive bool + + stageHello chan struct{} + stageHash chan struct{} ready chan struct{} - cancel chan struct{} + recver chan struct{} + sender chan struct{} tmr *time.Timer } -var errDupSalt = errors.New("peer repeats salt") -var errAuthFail = errors.New("peer auth fail") +var errAuthFail = errors.New("mismatch auth hash") +var errAuthUnknown = errors.New("unknown auth peer") var errTimeout = errors.New("timeout") -func (a *auth) Init() error { - b := make([]byte, saltSize) - if _, err := rand.Read(b); err != nil { - return err +func hashsum(args ...string) string { + h := md5.New() + + for _, s := range args { + io.WriteString(h, s) } - a.salt.self = string(b) + return string(h.Sum(nil)) +} - a.h.m.Store(a.salt.self, struct{}{}) +func getpass(f *os.File, salt string, user string) string { + f.Seek(0, 0) + + for scanner := bufio.NewScanner(f); scanner.Scan(); { + splitted := strings.SplitN(scanner.Text(), "#", 2) + tokens := strings.Fields(splitted[0]) + if len(tokens) > 1 { + if salt == "" { + if tokens[0] == user { + return tokens[1] + } + } else { + if hashsum(salt, tokens[0]) == user { + return tokens[1] + } + } + } + } - return nil + return "" } -func (a *auth) hashSum(c string) string { - h := md5.New() +func (a *auth) Init(authfile string, user string) error { + file, err := os.Open(authfile) + if err != nil { + return fmt.Errorf("authfile: %w", err) + } - io.WriteString(h, c) - io.WriteString(h, a.secret) + a.file = file - return string(h.Sum(nil)) + if user != "" { + pass := getpass(file, "", user) + if pass == "" { + file.Close() + return fmt.Errorf("authfile: no pass for user '%s'", user) + } + a.user = user + a.pass = pass + } + + b := make([]byte, saltSize) + if _, err := rand.Read(b); err != nil { + return err + } + a.salt = string(b) + + a.tmr = time.NewTimer(authTimeout) + + return nil } func (a *auth) sync(c chan struct{}) error { select { case <-a.tmr.C: return errTimeout - case <-a.cancel: + case <-a.recver: + return io.EOF + case <-a.sender: return io.EOF case <-c: return nil @@ -82,24 +132,46 @@ func (a *auth) sync(c chan struct{}) error { func (a *auth) Send(rq, wq queue.Q) error { w := wq.Writer() - io.WriteString(w, a.salt.self) + io.WriteString(w, a.salt) + if a.passive { + io.WriteString(w, hashNone) + } else { + io.WriteString(w, hashsum(a.salt, a.user)) + } - if err := a.sync(a.readSaltDone); err != nil { + if err := a.sync(a.stageHello); err != nil { return err } - if _, ok := a.h.m.Load(a.salt.peer); ok { - return errDupSalt + if a.passive && a.peer.user == hashNone { + close(a.sender) + return fmt.Errorf("can not be passive together") } - io.WriteString(w, a.hashSum(a.salt.peer)) + var pass string - if err := a.sync(a.readHashDone); err != nil { + if a.peer.user == hashNone { + pass = hashsum(a.pass) + } else { + pass = getpass(a.file, a.peer.salt, a.peer.user) + if pass == "" { + close(a.sender) + return fmt.Errorf("no pass for peer") + } + + if a.passive { + a.pass = hashsum(pass) + } + } + + io.WriteString(w, hashsum(a.peer.salt, pass)) + + if err := a.sync(a.stageHash); err != nil { return err } - if a.hash != a.hashSum(a.salt.self) { - close(a.cancel) + if a.hash != hashsum(a.salt, a.pass) { + close(a.sender) return errAuthFail } @@ -112,29 +184,32 @@ func (a *auth) read(r io.Reader, n int, s *string) error { b := make([]byte, n) if _, err := io.ReadFull(r, b); err != nil { - close(a.cancel) + close(a.recver) return err } *s = string(b) - return nil } func (a *auth) Recv(rq, wq queue.Q) error { r := rq.Reader() - if err := a.read(r, saltSize, &a.salt.peer); err != nil { + if err := a.read(r, saltSize, &a.peer.salt); err != nil { return err } - close(a.readSaltDone) + if err := a.read(r, hashSize, &a.peer.user); err != nil { + return err + } + + close(a.stageHello) if err := a.read(r, hashSize, &a.hash); err != nil { return err } - close(a.readHashDone) + close(a.stageHash) if err := a.sync(a.ready); err != nil { return err @@ -146,23 +221,38 @@ func (a *auth) Recv(rq, wq queue.Q) error { } func (a *auth) Close() { - a.h.m.Delete(a.salt.self) + a.file.Close() } func (h *authHook) New(env env.Env) (interface{}, error) { + file := env.Value("authfile") + + if file == "" { + return nil, errors.New("no authfile configured") + } + + user := "" + + if !h.Passive { + user = env.Value("authuser") + + if user == "" { + return nil, errors.New("no authuser configured") + } + } + a := &auth{ - h: h, - secret: env.Value("secret"), - tmr: time.NewTimer(authTimeout), + passive: h.Passive, - cancel: make(chan struct{}), ready: make(chan struct{}), + recver: make(chan struct{}), + sender: make(chan struct{}), - readSaltDone: make(chan struct{}), - readHashDone: make(chan struct{}), + stageHello: make(chan struct{}), + stageHash: make(chan struct{}), } - if err := a.Init(); err != nil { + if err := a.Init(file, user); err != nil { return nil, err } @@ -170,5 +260,5 @@ func (h *authHook) New(env env.Env) (interface{}, error) { } func init() { - register("auth", "chap authentication out/in", authHook{}) + register("auth", "chap out/in", authHook{}) } diff --git a/pkg/server/hook/proxy.go b/pkg/server/hook/proxy.go index 0a79056..4e30105 100644 --- a/pkg/server/hook/proxy.go +++ b/pkg/server/hook/proxy.go @@ -11,7 +11,7 @@ import ( "tunnel/pkg/server/queue" ) -var addrPattern = "^([0-9a-zA-Z-.]+|\\[[0-9a-fA-F:]*\\]):[0-9]+$" +var addrPattern = "^(([0-9a-zA-Z-.]+|\\[[0-9a-fA-F:]*\\]):|%)[0-9]+$" var isGoodAddr = regexp.MustCompile(addrPattern).MatchString type proxyHook struct { diff --git a/pkg/server/socket/socket.go b/pkg/server/socket/socket.go index cb76cf7..6768c48 100644 --- a/pkg/server/socket/socket.go +++ b/pkg/server/socket/socket.go @@ -103,8 +103,8 @@ func New(desc string, e env.Env) (S, error) { func parseProtoAddr(proto, addr string) (string, string) { if proto == "tcp" || proto == "udp" { - if strings.HasPrefix(addr, "-:") { - addr = "localhost" + addr[1:] + if strings.HasPrefix(addr, "%") { + addr = "localhost:" + addr[1:] } } diff --git a/pkg/test/auth_test.go b/pkg/test/auth_test.go index dedafa8..1741d68 100644 --- a/pkg/test/auth_test.go +++ b/pkg/test/auth_test.go @@ -2,6 +2,8 @@ package test import ( "testing" + "fmt" + "os" ) func TestAuthHook(t *testing.T) { @@ -10,11 +12,69 @@ func TestAuthHook(t *testing.T) { c := e.newInstance() - c.Exec("add name T listen,addr=-:0 auth aes dial,addr=@[tunnel.X.listen]") - c.Exec("add name X listen,addr=-:0 /aes /auth dial,addr=@[addr]") + var secrets string - c.Exec("set tunnel.X.secret secret") - c.Exec("set tunnel.T.secret secret") + f, err := os.CreateTemp("", "test-auth-") + if err != nil { + e.Fatalf("create temp: %v", err) + } + + secrets = f.Name() + + fmt.Fprintln(f, "T t") + fmt.Fprintln(f, "X x") + f.Close() + + defer os.Remove(secrets) + + c.Exec("add name T listen,addr=%%0 auth aes dial,addr=@[tunnel.X.listen]") + c.Exec("add name X listen,addr=%%0 /aes /auth dial,addr=@[addr]") + + c.Exec("set authfile %s", secrets) + c.Exec("set tunnel.T.authuser T") + c.Exec("set tunnel.X.authuser X") + + listen := e.Listen("tcp", "127.0.0.1:0") + c.Set("addr", listen.Addr()) + + out := e.Dial("tcp", c.Get("tunnel.T.listen")) + in := e.Accept(listen) + + e.Write(out, dummy) + + buf := make([]byte, len(dummy)) + e.ReadFull(in, buf) + + if r := string(buf); r != dummy { + e.Fatalf("wrong reply: send '%s', recv '%s'", dummy, r) + } +} + +func TestAuthPassiveHook(t *testing.T) { + e := newEnv(t) + defer e.Free() + + c := e.newInstance() + + var secrets string + + f, err := os.CreateTemp("", "test-auth-passive-") + if err != nil { + e.Fatalf("create temp: %v", err) + } + + secrets = f.Name() + + fmt.Fprintln(f, "T t") + f.Close() + + defer os.Remove(secrets) + + c.Exec("add name T listen,addr=%%0 auth aes dial,addr=@[tunnel.X.listen]") + c.Exec("add name X listen,addr=%%0 /aes /auth,passive dial,addr=@[addr]") + + c.Exec("set authfile %s", secrets) + c.Exec("set tunnel.T.authuser T") listen := e.Listen("tcp", "127.0.0.1:0") c.Set("addr", listen.Addr()) diff --git a/pkg/test/defer_test.go b/pkg/test/defer_test.go index 57854b2..3e51571 100644 --- a/pkg/test/defer_test.go +++ b/pkg/test/defer_test.go @@ -10,7 +10,7 @@ func TestDeferSocket(t *testing.T) { c := e.newInstance() - c.Exec("add name T listen,addr=-:0 defer,addr=@[addr]") + c.Exec("add name T listen,addr=%%0 defer,addr=@[addr]") out := e.Dial("tcp", c.Get("tunnel.T.listen")) diff --git a/pkg/test/exec_test.go b/pkg/test/exec_test.go index c8d61a0..028b206 100644 --- a/pkg/test/exec_test.go +++ b/pkg/test/exec_test.go @@ -12,7 +12,7 @@ func TestExec(t *testing.T) { c := e.newInstance() - c.Exec("add name T listen,addr=-:0 upper exec,cmd=cat") + c.Exec("add name T listen,addr=%%0 upper exec,cmd=cat") conn := e.Dial("tcp", c.Get("tunnel.T.listen")) diff --git a/pkg/test/hook_test.go b/pkg/test/hook_test.go index 7808883..8e4fa4d 100644 --- a/pkg/test/hook_test.go +++ b/pkg/test/hook_test.go @@ -13,7 +13,7 @@ func TestUpperHook(t *testing.T) { c := e.newInstance() - c.Exec("add name T listen,addr=-:0 upper loop") + c.Exec("add name T listen,addr=%%0 upper loop") conn := e.Dial("tcp", c.Get("tunnel.T.listen")) @@ -33,7 +33,7 @@ func TestHexHook(t *testing.T) { c := e.newInstance() - c.Exec("add name T listen,addr=-:0 hex dial,addr=@[addr]") + c.Exec("add name T listen,addr=%%0 hex dial,addr=@[addr]") listen := e.Listen("tcp", "127.0.0.1:0") c.Set("addr", listen.Addr()) diff --git a/pkg/test/proxy_test.go b/pkg/test/proxy_test.go index ae89b0a..46782d2 100644 --- a/pkg/test/proxy_test.go +++ b/pkg/test/proxy_test.go @@ -10,8 +10,8 @@ func TestProxyHook(t *testing.T) { c := e.newInstance() - c.Exec("add name C listen,addr=-:0 proxy,addr=@[addr] dial,addr=@[tunnel.S.listen]") - c.Exec("add name S listen,addr=-:0 proxy") + c.Exec("add name C listen,addr=%%0 proxy,addr=@[addr] dial,addr=@[tunnel.S.listen]") + c.Exec("add name S listen,addr=%%0 proxy") c.Exec("set tunnel.S.proxy.auth user:password") c.Exec("set tunnel.C.proxy.auth user:password") -- cgit v1.2.3-70-g09d2