2016-06-01 04:07:09 +00:00
|
|
|
// Package tcp is used to perform TCP handshake without ACK.
|
|
|
|
// Useful for health checking, HAProxy do this exactly the same.
|
|
|
|
// Which is SYN, SYN-ACK, RST.
|
|
|
|
//
|
|
|
|
// Why do I have to do this?
|
|
|
|
// Usually when you establish a TCP connection(e.g. net.Dial), these
|
|
|
|
// are the first three packets (TCP three-way handshake):
|
|
|
|
//
|
|
|
|
// SYN: Client -> Server
|
|
|
|
// SYN-ACK: Server -> Client
|
|
|
|
// ACK: Client -> Server
|
|
|
|
//
|
|
|
|
// This package tries to avoid the last ACK when doing handshakes.
|
|
|
|
//
|
|
|
|
// By sending the last ACK, the connection is considered established.
|
|
|
|
// However as for TCP health checking the last ACK may not necessary.
|
|
|
|
// The Server could be considered alive after it sends back SYN-ACK.
|
|
|
|
//
|
|
|
|
// Benefits of avoiding the last ACK:
|
|
|
|
//
|
|
|
|
// 1. Less packets better efficiency
|
|
|
|
//
|
|
|
|
// 2. The health checking is less obvious
|
|
|
|
//
|
|
|
|
// The second one is essential, because it bothers server less.
|
|
|
|
// Usually this means the server will not notice the health checking
|
|
|
|
// traffic at all, thus the act of health chekcing will not be
|
|
|
|
// considered as some misbehaviour of client.
|
2016-06-01 09:45:24 +00:00
|
|
|
//
|
|
|
|
// Shaker's methods may be called by multiple goroutines simultaneously.
|
2016-06-01 04:07:09 +00:00
|
|
|
package tcp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"os"
|
|
|
|
"runtime"
|
2016-06-01 09:45:24 +00:00
|
|
|
"sync"
|
2016-06-01 04:07:09 +00:00
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const maxEpollEvents = 32
|
|
|
|
|
|
|
|
// Shaker contains an epoll instance for TCP handshake checking
|
|
|
|
type Shaker struct {
|
2016-06-01 09:45:24 +00:00
|
|
|
sync.RWMutex
|
2016-06-01 04:07:09 +00:00
|
|
|
epollFd int
|
|
|
|
}
|
|
|
|
|
|
|
|
// Init creates inner epoll instance, call this before anything else
|
|
|
|
func (s *Shaker) Init() error {
|
|
|
|
var err error
|
2016-06-01 09:45:24 +00:00
|
|
|
s.Lock()
|
|
|
|
defer s.Unlock()
|
|
|
|
if s.epollFd > 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-06-01 04:07:09 +00:00
|
|
|
s.epollFd, err = syscall.EpollCreate1(0)
|
|
|
|
if err != nil {
|
|
|
|
return os.NewSyscallError("epoll_create1", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test performs a TCP check with given TCP address and timeout
|
|
|
|
// A successful check will result in nil error
|
|
|
|
// ErrTimeout is returned if timeout
|
|
|
|
// Note: timeout includes domain resolving
|
|
|
|
func (s *Shaker) Test(addr string, timeout time.Duration) error {
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
|
|
|
|
rAddr, err := parseSockAddr(addr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fd, err := createSocket()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer syscall.Close(fd)
|
|
|
|
|
|
|
|
if err = setSockopts(fd); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = s.connect(fd, rAddr, deadline); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if reached(deadline) {
|
|
|
|
return ErrTimeout
|
|
|
|
}
|
|
|
|
|
|
|
|
s.addEpoll(fd)
|
|
|
|
timeoutMS := int(timeout.Nanoseconds() / 1000000)
|
|
|
|
// check for connect error
|
|
|
|
for {
|
|
|
|
succeed, err := s.wait(fd, timeoutMS)
|
|
|
|
if reached(deadline) {
|
|
|
|
return ErrTimeout
|
|
|
|
}
|
2016-06-01 09:47:04 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-06-01 04:07:09 +00:00
|
|
|
if succeed {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-01 09:46:36 +00:00
|
|
|
// Ready returns a bool indicates whether the Shaker is ready for use
|
|
|
|
func (s *Shaker) Ready() bool {
|
|
|
|
s.RLock()
|
|
|
|
defer s.RUnlock()
|
|
|
|
return s.epollFd > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// EpollFd returns the inner fd of epoll instance
|
|
|
|
// Note: Use this only when you really know what you are doing
|
|
|
|
func (s *Shaker) EpollFd() int {
|
|
|
|
s.RLock()
|
|
|
|
defer s.RUnlock()
|
|
|
|
return s.epollFd
|
|
|
|
}
|
|
|
|
|
2016-06-01 04:07:09 +00:00
|
|
|
// Close closes the inner epoll fd
|
|
|
|
func (s *Shaker) Close() error {
|
2016-06-01 09:45:24 +00:00
|
|
|
s.Lock()
|
|
|
|
defer s.Unlock()
|
|
|
|
if s.epollFd > 0 {
|
|
|
|
err := syscall.Close(s.epollFd)
|
|
|
|
s.epollFd = 0
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2016-06-01 04:07:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Shaker) addEpoll(fd int) error {
|
|
|
|
var event syscall.EpollEvent
|
|
|
|
event.Events = syscall.EPOLLOUT
|
|
|
|
event.Fd = int32(fd)
|
2016-06-01 09:45:24 +00:00
|
|
|
s.RLock()
|
|
|
|
defer s.RUnlock()
|
2016-06-01 04:07:09 +00:00
|
|
|
if err := syscall.EpollCtl(s.epollFd, syscall.EPOLL_CTL_ADD, fd, &event); err != nil {
|
|
|
|
return os.NewSyscallError("epoll_ctl", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// wait waits for epoll event of given fd
|
|
|
|
// The boolean returned indicates whether the connect is successful
|
|
|
|
func (s *Shaker) wait(fd int, timeoutMS int) (bool, error) {
|
|
|
|
var events [maxEpollEvents]syscall.EpollEvent
|
2016-06-01 09:45:24 +00:00
|
|
|
s.RLock()
|
|
|
|
epollFd := s.epollFd
|
|
|
|
if epollFd <= 0 {
|
|
|
|
return false, ErrNotInitialized
|
|
|
|
}
|
|
|
|
s.RUnlock()
|
|
|
|
nevents, err := syscall.EpollWait(epollFd, events[:], timeoutMS)
|
2016-06-01 04:07:09 +00:00
|
|
|
if err != nil {
|
|
|
|
return false, os.NewSyscallError("epoll_wait", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for ev := 0; ev < nevents; ev++ {
|
|
|
|
if int(events[ev].Fd) == fd {
|
|
|
|
errCode, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_ERROR)
|
|
|
|
if err != nil {
|
|
|
|
return false, os.NewSyscallError("getsockopt", err)
|
|
|
|
}
|
|
|
|
if errCode != 0 {
|
2016-06-01 09:47:04 +00:00
|
|
|
return false, newErrConnect(errCode)
|
2016-06-01 04:07:09 +00:00
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Shaker) connect(fd int, addr syscall.Sockaddr, deadline time.Time) error {
|
|
|
|
switch err := syscall.Connect(fd, addr); err {
|
|
|
|
case syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR:
|
|
|
|
case nil, syscall.EISCONN:
|
|
|
|
// already connected
|
|
|
|
case syscall.EINVAL:
|
|
|
|
// On Solaris we can see EINVAL if the socket has
|
|
|
|
// already been accepted and closed by the server.
|
|
|
|
// Treat this as a successful connection--writes to
|
|
|
|
// the socket will see EOF. For details and a test
|
|
|
|
// case in C see https://golang.org/issue/6828.
|
|
|
|
if runtime.GOOS == "solaris" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
fallthrough
|
|
|
|
default:
|
|
|
|
return os.NewSyscallError("connect", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func reached(deadline time.Time) bool {
|
|
|
|
return !deadline.IsZero() && deadline.Before(time.Now())
|
|
|
|
}
|