-
Notifications
You must be signed in to change notification settings - Fork 189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use the IP PacketConn to specify the local proxy IP #110
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,7 +72,7 @@ func startTCPEchoServer(t testing.TB) (*net.TCPListener, *sync.WaitGroup) { | |
} | ||
|
||
func startUDPEchoServer(t testing.TB) (*net.UDPConn, *sync.WaitGroup) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be great to exercise the new behavior somehow, so we can be confident it's not breaking things. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For testing, perhaps you can listen on two separate IPs, like 127.0.0.1 and 127.0.0.2? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was able to test using 127.0.0.1 and 127.0.0.2, but I can't easily write IPv6 tests because (1) IPv6 doesn't have an analogue to 127.0.0.2 and (2) my workstation (and presumably some other potential tests environments) has no IPv6 support. |
||
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
if err != nil { | ||
t.Fatalf("Proxy ListenUDP failed: %v", err) | ||
} | ||
|
@@ -256,7 +256,7 @@ func (m *fakeUDPMetrics) RemoveUDPNatEntry() { | |
func TestUDPEcho(t *testing.T) { | ||
echoConn, echoRunning := startUDPEchoServer(t) | ||
|
||
proxyConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
proxyConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
if err != nil { | ||
t.Fatalf("ListenTCP failed: %v", err) | ||
} | ||
|
@@ -496,7 +496,7 @@ func BenchmarkTCPMultiplexing(b *testing.B) { | |
func BenchmarkUDPEcho(b *testing.B) { | ||
echoConn, echoRunning := startUDPEchoServer(b) | ||
|
||
proxyConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
proxyConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
if err != nil { | ||
b.Fatalf("ListenTCP failed: %v", err) | ||
} | ||
|
@@ -544,7 +544,7 @@ func BenchmarkUDPEcho(b *testing.B) { | |
func BenchmarkUDPManyKeys(b *testing.B) { | ||
echoConn, echoRunning := startUDPEchoServer(b) | ||
|
||
proxyConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
proxyConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
if err != nil { | ||
b.Fatalf("ListenTCP failed: %v", err) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,14 @@ | ||
package net | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net" | ||
"runtime" | ||
|
||
"golang.org/x/net/ipv4" | ||
"golang.org/x/net/ipv6" | ||
) | ||
|
||
// DuplexConn is a net.Conn that allows for closing only the reader or writer end of | ||
|
@@ -97,3 +103,63 @@ type ConnectionError struct { | |
func NewConnectionError(status, message string, cause error) *ConnectionError { | ||
return &ConnectionError{Status: status, Message: message, Cause: cause} | ||
} | ||
|
||
// ReadFromWithDst reads one packet from `conn` into `b` and returns the number | ||
// of bytes read, the source address, and the destination IP address. It enables | ||
// recovery of the destination IP, which is otherwise lost for UDP connections | ||
// that are bound to `0.0.0.0` or `::`. | ||
func ReadFromWithDst(conn net.PacketConn, b []byte) (n int, src *net.UDPAddr, dst net.IP, err error) { | ||
var tmpSrc net.Addr | ||
if conn.LocalAddr().Network() == "udp4" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan of this type switch because you already have per type code when you create the UDP services and because the Instead, you could introduce the concept of a You can have separate factory functions for IP 4 and 6, so you won't need the type switch. The higher-level concept will make the code easier to read because it will allow us to understand the different concerns separately. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I've fully separated the IPv4 and IPv6 factories. |
||
ipv4Conn := ipv4.NewPacketConn(conn) | ||
if err = ipv4Conn.SetControlMessage(ipv4.FlagDst, true); err != nil { | ||
return | ||
} | ||
var cm *ipv4.ControlMessage | ||
if n, cm, tmpSrc, err = ipv4Conn.ReadFrom(b); err != nil { | ||
return | ||
} | ||
if cm != nil { | ||
dst = cm.Dst | ||
} else if runtime.GOOS != "windows" { | ||
err = errors.New("control data is missing") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this error for Windows only? It seems that for any system the control data would be missing at this point. Same for ipv6. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The x/net/ipv* libraries do not support control data on Windows, so |
||
return | ||
} | ||
} else if conn.LocalAddr().Network() == "udp6" { | ||
ipv6Conn := ipv6.NewPacketConn(conn) | ||
if err = ipv6Conn.SetControlMessage(ipv6.FlagDst, true); err != nil { | ||
return | ||
} | ||
var cm *ipv6.ControlMessage | ||
if n, cm, tmpSrc, err = ipv6Conn.ReadFrom(b); err != nil { | ||
return | ||
} | ||
if cm != nil { | ||
dst = cm.Dst | ||
} else if runtime.GOOS != "windows" { | ||
err = errors.New("control data is missing") | ||
return | ||
} | ||
} else { | ||
err = fmt.Errorf("unsupported network: %s", conn.LocalAddr().Network()) | ||
return | ||
} | ||
src = tmpSrc.(*net.UDPAddr) | ||
return | ||
} | ||
|
||
// WriteToWithSrc sends `b` to `dst` on `conn` from the specified source IP. | ||
// This can be useful when the system has multiple IP addresses of the same family. | ||
// Similar functionality can be achieved by binding a new UDP socket to a specific local address, | ||
// but that might run into problems if the port is already bound by an existing socket. | ||
func WriteToWithSrc(conn net.PacketConn, b []byte, src net.IP, dst *net.UDPAddr) (int, error) { | ||
if conn.LocalAddr().Network() == "udp4" { | ||
cm := &ipv4.ControlMessage{Src: src} | ||
return ipv4.NewPacketConn(conn).WriteTo(b, cm, dst) | ||
} else if conn.LocalAddr().Network() == "udp6" { | ||
cm := &ipv6.ControlMessage{Src: src} | ||
return ipv6.NewPacketConn(conn).WriteTo(b, cm, dst) | ||
} else { | ||
return 0, fmt.Errorf("unsupported network: %s", conn.LocalAddr().Network()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -135,7 +135,7 @@ func (s *udpService) Serve(clientConn net.PacketConn) error { | |
}() | ||
|
||
// Attempt to read an upstream packet. | ||
clientProxyBytes, clientAddr, err := clientConn.ReadFrom(cipherBuf) | ||
clientProxyBytes, clientAddr, proxyIP, err := onet.ReadFromWithDst(clientConn, cipherBuf) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is intrusive and adds another thing to keep track of. I think we can do better by composing behavior. You will still need to expose those fields for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A full version of this would require moving the NAT code from After considering various options, I think the current design provides the best balance of simplicity and performance. I've reorganized the functionality to present a cleaner interface, but I have not attempted to provide any "virtual Conn" wrapping. |
||
if err != nil { | ||
s.mu.RLock() | ||
stopped = s.stopped | ||
|
@@ -171,7 +171,7 @@ func (s *udpService) Serve(clientConn net.PacketConn) error { | |
cipherData := cipherBuf[:clientProxyBytes] | ||
var payload []byte | ||
var tgtUDPAddr *net.UDPAddr | ||
targetConn := nm.Get(clientAddr.String()) | ||
targetConn := nm.Get(clientAddr, proxyIP) | ||
if targetConn == nil { | ||
var locErr error | ||
clientLocation, locErr = s.m.GetLocation(clientAddr) | ||
|
@@ -180,7 +180,7 @@ func (s *udpService) Serve(clientConn net.PacketConn) error { | |
} | ||
debugUDPAddr(clientAddr, "Got location \"%s\"", clientLocation) | ||
|
||
ip := clientAddr.(*net.UDPAddr).IP | ||
ip := clientAddr.IP | ||
var textData []byte | ||
var cipher *ss.Cipher | ||
unpackStart := time.Now() | ||
|
@@ -200,7 +200,7 @@ func (s *udpService) Serve(clientConn net.PacketConn) error { | |
if err != nil { | ||
return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create UDP socket", err) | ||
} | ||
targetConn = nm.Add(clientAddr, clientConn, cipher, udpConn, clientLocation, keyID) | ||
targetConn = nm.Add(clientAddr, proxyIP, clientConn, cipher, udpConn, clientLocation, keyID) | ||
} else { | ||
clientLocation = targetConn.clientLocation | ||
|
||
|
@@ -335,29 +335,38 @@ func (c *natconn) ReadFrom(buf []byte) (int, net.Addr, error) { | |
return n, addr, err | ||
} | ||
|
||
type natkey struct { | ||
clientAddr string // TODO: Use netip.AddrPort | ||
proxyIP string // TODO: Use netip.Addr | ||
} | ||
|
||
func makeNATKey(clientAddr *net.UDPAddr, proxyIP net.IP) natkey { | ||
return natkey{clientAddr.String(), proxyIP.String()} | ||
} | ||
|
||
// Packet NAT table | ||
type natmap struct { | ||
sync.RWMutex | ||
keyConn map[string]*natconn | ||
keyConn map[natkey]*natconn | ||
timeout time.Duration | ||
metrics metrics.ShadowsocksMetrics | ||
running *sync.WaitGroup | ||
} | ||
|
||
func newNATmap(timeout time.Duration, sm metrics.ShadowsocksMetrics, running *sync.WaitGroup) *natmap { | ||
m := &natmap{metrics: sm, running: running} | ||
m.keyConn = make(map[string]*natconn) | ||
m.keyConn = make(map[natkey]*natconn) | ||
m.timeout = timeout | ||
return m | ||
} | ||
|
||
func (m *natmap) Get(key string) *natconn { | ||
func (m *natmap) Get(clientAddr *net.UDPAddr, proxyIP net.IP) *natconn { | ||
m.RLock() | ||
defer m.RUnlock() | ||
return m.keyConn[key] | ||
return m.keyConn[makeNATKey(clientAddr, proxyIP)] | ||
} | ||
|
||
func (m *natmap) set(key string, pc net.PacketConn, cipher *ss.Cipher, keyID, clientLocation string) *natconn { | ||
func (m *natmap) set(key natkey, pc net.PacketConn, cipher *ss.Cipher, keyID, clientLocation string) *natconn { | ||
entry := &natconn{ | ||
PacketConn: pc, | ||
cipher: cipher, | ||
|
@@ -373,7 +382,7 @@ func (m *natmap) set(key string, pc net.PacketConn, cipher *ss.Cipher, keyID, cl | |
return entry | ||
} | ||
|
||
func (m *natmap) del(key string) net.PacketConn { | ||
func (m *natmap) del(key natkey) net.PacketConn { | ||
m.Lock() | ||
defer m.Unlock() | ||
|
||
|
@@ -385,15 +394,16 @@ func (m *natmap) del(key string) net.PacketConn { | |
return nil | ||
} | ||
|
||
func (m *natmap) Add(clientAddr net.Addr, clientConn net.PacketConn, cipher *ss.Cipher, targetConn net.PacketConn, clientLocation, keyID string) *natconn { | ||
entry := m.set(clientAddr.String(), targetConn, cipher, keyID, clientLocation) | ||
func (m *natmap) Add(clientAddr *net.UDPAddr, proxyIP net.IP, clientConn net.PacketConn, cipher *ss.Cipher, targetConn net.PacketConn, clientLocation, keyID string) *natconn { | ||
key := makeNATKey(clientAddr, proxyIP) | ||
entry := m.set(key, targetConn, cipher, keyID, clientLocation) | ||
|
||
m.metrics.AddUDPNatEntry() | ||
m.running.Add(1) | ||
go func() { | ||
timedCopy(clientAddr, clientConn, entry, keyID, m.metrics) | ||
timedCopy(clientAddr, proxyIP, clientConn, entry, keyID, m.metrics) | ||
m.metrics.RemoveUDPNatEntry() | ||
if pc := m.del(clientAddr.String()); pc != nil { | ||
if pc := m.del(key); pc != nil { | ||
pc.Close() | ||
} | ||
m.running.Done() | ||
|
@@ -420,7 +430,7 @@ func (m *natmap) Close() error { | |
var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345")) | ||
|
||
// copy from target to client until read timeout | ||
func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn *natconn, | ||
func timedCopy(clientAddr *net.UDPAddr, proxyIP net.IP, clientConn net.PacketConn, targetConn *natconn, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could take the bound There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed this to take a |
||
keyID string, sm metrics.ShadowsocksMetrics) { | ||
// pkt is used for in-place encryption of downstream UDP packets, with the layout | ||
// [padding?][salt][address][body][tag][extra] | ||
|
@@ -475,7 +485,7 @@ func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn *natco | |
if err != nil { | ||
return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err) | ||
} | ||
proxyClientBytes, err = clientConn.WriteTo(buf, clientAddr) | ||
proxyClientBytes, err = onet.WriteToWithSrc(clientConn, buf, proxyIP, clientAddr) | ||
if err != nil { | ||
return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uses: codecov/codecov-action@v2.1.0