Skip to content

Commit 0c70074

Browse files
committed
First commit
1 parent 69c453c commit 0c70074

File tree

5 files changed

+275
-0
lines changed

5 files changed

+275
-0
lines changed

README.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# TCP Shaker :heartbeat:
2+
[![GoDoc](https://godoc.org/github.com/tevino/tcp-shaker?status.svg)](https://godoc.org/github.com/tevino/tcp-shaker)
3+
4+
Performing TCP handshake without ACK, useful for health checking.
5+
6+
HAProxy do this exactly the same, which is:
7+
8+
- SYN
9+
- SYN-ACK
10+
- RST
11+
12+
## Why do I have to do this?
13+
Usually when you establish a TCP connection(e.g. net.Dial), these are the first three packets (TCP three-way handshake):
14+
15+
- Client -> Server: SYN
16+
- Server -> Client: SYN-ACK
17+
- Client -> Server: ACK
18+
19+
**This package tries to avoid the last ACK when doing handshakes.**
20+
21+
By sending the last ACK, the connection is considered established.
22+
23+
However as for TCP health checking the last ACK may not necessary.
24+
25+
The Server could be considered alive after it sends back SYN-ACK.
26+
27+
### Benefits of avoiding the last ACK:
28+
1. Less packets better efficiency
29+
2. The health checking is less obvious
30+
31+
The second one is essential, because it bothers server less.
32+
33+
Usually this means the server will not notice the health checking traffic at all, **thus the act of health chekcing will not be
34+
considered as some misbehaviour of client.**
35+
36+
## Requirements:
37+
- Linux 2.4 or newer
38+
39+
## TODO:
40+
41+
- [ ] IPv6 support (Test environment needed, PRs are welcomed)

err.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package tcp
2+
3+
// ErrTimeout indicates I/O timeout
4+
var ErrTimeout = &timeoutError{}
5+
6+
type timeoutError struct{}
7+
8+
func (e *timeoutError) Error() string { return "I/O timeout" }
9+
func (e *timeoutError) Timeout() bool { return true }
10+
func (e *timeoutError) Temporary() bool { return true }

shaker.go

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Package tcp is used to perform TCP handshake without ACK.
2+
// Useful for health checking, HAProxy do this exactly the same.
3+
// Which is SYN, SYN-ACK, RST.
4+
//
5+
// Why do I have to do this?
6+
// Usually when you establish a TCP connection(e.g. net.Dial), these
7+
// are the first three packets (TCP three-way handshake):
8+
//
9+
// SYN: Client -> Server
10+
// SYN-ACK: Server -> Client
11+
// ACK: Client -> Server
12+
//
13+
// This package tries to avoid the last ACK when doing handshakes.
14+
//
15+
// By sending the last ACK, the connection is considered established.
16+
// However as for TCP health checking the last ACK may not necessary.
17+
// The Server could be considered alive after it sends back SYN-ACK.
18+
//
19+
// Benefits of avoiding the last ACK:
20+
//
21+
// 1. Less packets better efficiency
22+
//
23+
// 2. The health checking is less obvious
24+
//
25+
// The second one is essential, because it bothers server less.
26+
// Usually this means the server will not notice the health checking
27+
// traffic at all, thus the act of health chekcing will not be
28+
// considered as some misbehaviour of client.
29+
package tcp
30+
31+
import (
32+
"fmt"
33+
"os"
34+
"runtime"
35+
"syscall"
36+
"time"
37+
)
38+
39+
const maxEpollEvents = 32
40+
41+
// Shaker contains an epoll instance for TCP handshake checking
42+
type Shaker struct {
43+
epollFd int
44+
}
45+
46+
// Init creates inner epoll instance, call this before anything else
47+
func (s *Shaker) Init() error {
48+
var err error
49+
s.epollFd, err = syscall.EpollCreate1(0)
50+
if err != nil {
51+
return os.NewSyscallError("epoll_create1", err)
52+
}
53+
return nil
54+
}
55+
56+
// Test performs a TCP check with given TCP address and timeout
57+
// A successful check will result in nil error
58+
// ErrTimeout is returned if timeout
59+
// Note: timeout includes domain resolving
60+
func (s *Shaker) Test(addr string, timeout time.Duration) error {
61+
deadline := time.Now().Add(timeout)
62+
63+
rAddr, err := parseSockAddr(addr)
64+
if err != nil {
65+
return err
66+
}
67+
68+
fd, err := createSocket()
69+
if err != nil {
70+
return err
71+
}
72+
defer syscall.Close(fd)
73+
74+
if err = setSockopts(fd); err != nil {
75+
return err
76+
}
77+
78+
if err = s.connect(fd, rAddr, deadline); err != nil {
79+
return err
80+
}
81+
if reached(deadline) {
82+
return ErrTimeout
83+
}
84+
85+
s.addEpoll(fd)
86+
timeoutMS := int(timeout.Nanoseconds() / 1000000)
87+
// check for connect error
88+
for {
89+
succeed, err := s.wait(fd, timeoutMS)
90+
if err != nil {
91+
return fmt.Errorf("connect error: %s", err)
92+
}
93+
if reached(deadline) {
94+
return ErrTimeout
95+
}
96+
if succeed {
97+
return nil
98+
}
99+
}
100+
}
101+
102+
// Close closes the inner epoll fd
103+
func (s *Shaker) Close() error {
104+
return syscall.Close(s.epollFd)
105+
}
106+
107+
func (s *Shaker) addEpoll(fd int) error {
108+
var event syscall.EpollEvent
109+
event.Events = syscall.EPOLLOUT
110+
event.Fd = int32(fd)
111+
if err := syscall.EpollCtl(s.epollFd, syscall.EPOLL_CTL_ADD, fd, &event); err != nil {
112+
return os.NewSyscallError("epoll_ctl", err)
113+
}
114+
return nil
115+
}
116+
117+
// wait waits for epoll event of given fd
118+
// The boolean returned indicates whether the connect is successful
119+
func (s *Shaker) wait(fd int, timeoutMS int) (bool, error) {
120+
var events [maxEpollEvents]syscall.EpollEvent
121+
nevents, err := syscall.EpollWait(s.epollFd, events[:], timeoutMS)
122+
if err != nil {
123+
return false, os.NewSyscallError("epoll_wait", err)
124+
}
125+
126+
for ev := 0; ev < nevents; ev++ {
127+
if int(events[ev].Fd) == fd {
128+
errCode, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_ERROR)
129+
if err != nil {
130+
return false, os.NewSyscallError("getsockopt", err)
131+
}
132+
if errCode != 0 {
133+
return false, fmt.Errorf("getsockopt[%d]", errCode)
134+
}
135+
return true, nil
136+
}
137+
}
138+
return false, nil
139+
}
140+
141+
func (s *Shaker) connect(fd int, addr syscall.Sockaddr, deadline time.Time) error {
142+
switch err := syscall.Connect(fd, addr); err {
143+
case syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR:
144+
case nil, syscall.EISCONN:
145+
// already connected
146+
case syscall.EINVAL:
147+
// On Solaris we can see EINVAL if the socket has
148+
// already been accepted and closed by the server.
149+
// Treat this as a successful connection--writes to
150+
// the socket will see EOF. For details and a test
151+
// case in C see https://golang.org/issue/6828.
152+
if runtime.GOOS == "solaris" {
153+
return nil
154+
}
155+
fallthrough
156+
default:
157+
return os.NewSyscallError("connect", err)
158+
}
159+
return nil
160+
}
161+
162+
func reached(deadline time.Time) bool {
163+
return !deadline.IsZero() && deadline.Before(time.Now())
164+
}

shaker_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package tcp
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"time"
7+
)
8+
9+
func ExampleShaker() {
10+
s := Shaker{}
11+
if err := s.Init(); err != nil {
12+
log.Fatal("Shaker init failed:", err)
13+
}
14+
15+
timeout := time.Second * 1
16+
err := s.Test("google.com:80", timeout)
17+
switch err {
18+
case ErrTimeout:
19+
fmt.Println("Connect to Google timeout")
20+
case nil:
21+
fmt.Println("Connect to Google succeded")
22+
default:
23+
fmt.Println("Connect to Google failed:", err)
24+
}
25+
}

socket.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package tcp
2+
3+
import (
4+
"net"
5+
"syscall"
6+
)
7+
8+
func parseSockAddr(addr string) (syscall.Sockaddr, error) {
9+
tAddr, err := net.ResolveTCPAddr("tcp", addr)
10+
if err != nil {
11+
return nil, err
12+
}
13+
var addr4 [4]byte
14+
if tAddr.IP != nil {
15+
copy(addr4[:], tAddr.IP.To4()) // copy last 4 bytes of slice to array
16+
}
17+
return &syscall.SockaddrInet4{Port: tAddr.Port, Addr: addr4}, nil
18+
}
19+
20+
func createSocket() (int, error) {
21+
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
22+
syscall.CloseOnExec(fd)
23+
return fd, err
24+
}
25+
26+
func setSockopts(fd int) error {
27+
err := syscall.SetNonblock(fd, true)
28+
if err != nil {
29+
return err
30+
}
31+
32+
linger := syscall.Linger{Onoff: 1, Linger: 0}
33+
syscall.SetsockoptLinger(fd, syscall.SOL_SOCKET, syscall.SO_LINGER, &linger)
34+
return syscall.SetsockoptInt(fd, syscall.SOL_TCP, syscall.TCP_QUICKACK, 0)
35+
}

0 commit comments

Comments
 (0)