From 2a982f4de9c5d1df0baab8a58047673ebe5d9d35 Mon Sep 17 00:00:00 2001 From: amerril Date: Thu, 7 Nov 2024 17:49:22 -0700 Subject: [PATCH 01/11] POC: IP address rewriting for localhost access --- .gitignore | 1 + src/cmd/serve.go | 108 +++++++++++++++++++++++++++++++++++++++ src/transport/tcp/tcp.go | 4 +- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index dc1d65f..99ffce1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ bin/* !bin/.gitkeep src/dist +*.exe # macOS .DS_Store diff --git a/src/cmd/serve.go b/src/cmd/serve.go index 6e46af0..013cc68 100644 --- a/src/cmd/serve.go +++ b/src/cmd/serve.go @@ -21,6 +21,9 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" gtcp "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" gudp "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip" + _ "unsafe" //required to use the go:linkname directive "wiretap/peer" "wiretap/transport/api" @@ -64,6 +67,53 @@ type wiretapDefaultConfig struct { mtu int } +type portOrIdentRange struct { + start uint16 + size uint32 +} + +// Same as stack.DNATTarget, except it forwards ALL ports, not just one. +type DNAT_Target struct { + // The new destination address for packets. + // + // Immutable. + Addr tcpip.Address + + // NetworkProtocol is the network protocol the target is used with. + // + // Immutable. + NetworkProtocol tcpip.NetworkProtocolNumber +} + +// Same as stack.DNATTarget.Action(), except it forwards ALL ports, not just one. +func (rt *DNAT_Target) Action(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, addressEP stack.AddressableEndpoint) (stack.RuleVerdict, int) { + // Sanity check. + if rt.NetworkProtocol != pkt.NetworkProtocolNumber { + panic(fmt.Sprintf( + "DNATTarget.Action with NetworkProtocol %d called on packet with NetworkProtocolNumber %d", + rt.NetworkProtocol, pkt.NetworkProtocolNumber)) + } + + switch hook { + case stack.Prerouting, stack.Output: + case stack.Input, stack.Forward, stack.Postrouting: + panic(fmt.Sprintf("%s not supported for DNAT", hook)) + default: + panic(fmt.Sprintf("%s unrecognized", hook)) + } + + return dnat_Action(pkt, hook, r, rt.Addr) +} + +// Same as the unexported stack.dnatAction(), except it forwards ALL ports by using a large size value, instead of "1". +func dnat_Action(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, address tcpip.Address) (stack.RuleVerdict, int) { + return stack_natAction(pkt, hook, r, portOrIdentRange{start: 0, size: 65536}, address, true /* dnat */) +} + +// Janky unsafe compiler trick to give us access to the unexported stack.natAction() function to make the DNAT stuff work +//go:linkname stack_natAction gvisor.dev/gvisor/pkg/tcpip/stack.natAction +func stack_natAction(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, portsOrIdents portOrIdentRange, address tcpip.Address, dnat bool) (stack.RuleVerdict, int) + // Defaults for serve command. var serveCmd = serveCmdConfig{ configFile: "", @@ -484,6 +534,53 @@ func (c serveCmdConfig) Run() { } s.SetTransportProtocolHandler(gudp.ProtocolNumber, udp.Handler(udpConfig)) + /* + nics := s.NICInfo() + for k, v := range nics { + fmt.Printf("key %s: %+v \n", k, v) + } + */ + + // Playiung with IPTables + // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20230927004350-cbd86285d259/pkg/tcpip/stack + ipt := s.IPTables() + natTable := ipt.GetTable(stack.NATID, false) + /* All 5 seem to be the same (empty) + fmt.Println("Numnber of rules: ", len(natTable.Rules)) + for i := 0; i < len(natTable.Rules); i++ { + rule := natTable.Rules[i] + //fmt.Println(rule.Filter.InputInterface) + fmt.Printf("%+v\n", rule.Filter) + } + */ + //rule := natTable.Rules[0] + //fmt.Printf("%+v\n", natTable) + + // https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml + NetworkProtocolIPv4 := tcpip.NetworkProtocolNumber(2048) + //NetworkProtocolIPv6 := tcpip.NetworkProtocolNumber(34525) + + newFilter := EmptyFilter4() + newFilter.Dst = tcpip.AddrFromSlice([]byte{192,168,137,137}) + newFilter.DstMask = tcpip.AddrFromSlice([]byte{255,255,255,255}) + + newRule := new(stack.Rule) + newRule.Filter = newFilter + //fmt.Printf("%+v\n", newRule.Filter) + + //Don't need any additional matching conditions besides the IP filter + //newRule.Matchers = []stack.Matcher{testMatcher{}} + + //newRule.Target = &stack.DNATTarget{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), Port: 8000, NetworkProtocol: NetworkProtocolIPv4} + + // gvisor doesn't seem to provide access to the function primitives needed to do DNAT for all ports, so we have to roll our own. + newRule.Target = &DNAT_Target{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), NetworkProtocol: NetworkProtocolIPv4} + + // Not totally sure this is the right way to add the prerouting rule, but it seems to work + natTable.Rules[stack.Prerouting] = *newRule + ipt.ReplaceTable(stack.NATID, natTable, false) + + // Make new relay device. devRelay := device.NewDevice(tunRelay, conn.NewDefaultBind(), device.NewLogger(logger, "")) // Configure wireguard. @@ -535,3 +632,14 @@ func (c serveCmdConfig) Run() { wg.Wait() } + +// Borrowing this function from a more recent version of the module +// EmptyFilter4 returns an initialized IPv4 header filter. +func EmptyFilter4() stack.IPHeaderFilter { + return stack.IPHeaderFilter{ + Dst: tcpip.AddrFrom4([4]byte{}), + DstMask: tcpip.AddrFrom4([4]byte{}), + Src: tcpip.AddrFrom4([4]byte{}), + SrcMask: tcpip.AddrFrom4([4]byte{}), + } +} diff --git a/src/transport/tcp/tcp.go b/src/transport/tcp/tcp.go index 4225e4f..09c49f7 100644 --- a/src/transport/tcp/tcp.go +++ b/src/transport/tcp/tcp.go @@ -49,14 +49,14 @@ func Handler(c Config) func(*tcp.ForwarderRequest) { addr, _ := netip.AddrFromSlice(s.LocalAddress.AsSlice()) err := transport.GetConnCounts().AddAddress(addr, c.Tnet.Stack(), c.StackLock) if err != nil { - log.Println("failed to add address:", err) + log.Println("failed to add address: ", err) req.Complete(false) return } defer func() { err := transport.GetConnCounts().RemoveAddress(addr, c.Tnet.Stack(), c.StackLock) if err != nil { - log.Println("failed to remove address:", err) + log.Println("failed to remove address: ", err) } }() From 4821fae55f85ca8fa66febf0c86edc0161bbddc8 Mon Sep 17 00:00:00 2001 From: Aptimex Date: Fri, 8 Nov 2024 18:05:08 -0700 Subject: [PATCH 02/11] update gvisor, proper iptables DNAT; #66 --- src/cmd/serve.go | 37 ++++++++++++++++++++++++++++++++----- src/go.mod | 10 ++++++---- src/go.sum | 17 +++++++++++------ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/cmd/serve.go b/src/cmd/serve.go index 013cc68..c3b52c5 100644 --- a/src/cmd/serve.go +++ b/src/cmd/serve.go @@ -23,7 +23,7 @@ import ( gudp "gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip" - _ "unsafe" //required to use the go:linkname directive + //_ "unsafe" //required to use the go:linkname directive "wiretap/peer" "wiretap/transport/api" @@ -67,6 +67,23 @@ type wiretapDefaultConfig struct { mtu int } +/* +type testMatcher struct { + +} + +func (t testMatcher) Match(hook stack.Hook, packet stack.PacketBufferPtr, inputInterfaceName, outputInterfaceName string) (matches bool, hotdrop bool) { + fmt.Println("Checking for match") + if hook == stack.Prerouting { + fmt.Println("Match!") + return true, false + } + fmt.Println("No match") + return false, false +} +*/ + +/* type portOrIdentRange struct { start uint16 size uint32 @@ -107,12 +124,18 @@ func (rt *DNAT_Target) Action(pkt stack.PacketBufferPtr, hook stack.Hook, r *sta // Same as the unexported stack.dnatAction(), except it forwards ALL ports by using a large size value, instead of "1". func dnat_Action(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, address tcpip.Address) (stack.RuleVerdict, int) { - return stack_natAction(pkt, hook, r, portOrIdentRange{start: 0, size: 65536}, address, true /* dnat */) + //return stack_natAction(pkt, hook, r, portOrIdentRange{start: 0, size: 65536}, address, true) + fmt.Println("DNAT_action") + return stack_natAction(pkt, hook, r, portOrIdentRange{start: 8000, size: 1}, address, true, false, true) } // Janky unsafe compiler trick to give us access to the unexported stack.natAction() function to make the DNAT stuff work //go:linkname stack_natAction gvisor.dev/gvisor/pkg/tcpip/stack.natAction -func stack_natAction(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, portsOrIdents portOrIdentRange, address tcpip.Address, dnat bool) (stack.RuleVerdict, int) +func stack_natAction(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, portsOrIdents portOrIdentRange, address tcpip.Address, dnat, changePort, changeAddress bool) (stack.RuleVerdict, int) + +//old format +//func stack_natAction(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, portsOrIdents portOrIdentRange, address tcpip.Address, dnat bool) (stack.RuleVerdict, int) +*/ // Defaults for serve command. var serveCmd = serveCmdConfig{ @@ -574,11 +597,15 @@ func (c serveCmdConfig) Run() { //newRule.Target = &stack.DNATTarget{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), Port: 8000, NetworkProtocol: NetworkProtocolIPv4} // gvisor doesn't seem to provide access to the function primitives needed to do DNAT for all ports, so we have to roll our own. - newRule.Target = &DNAT_Target{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), NetworkProtocol: NetworkProtocolIPv4} + //newRule.Target = &DNAT_Target{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), NetworkProtocol: NetworkProtocolIPv4} + + //Do address-only DNAT; port remains the same, so all ports are effectively forwarded to localhost + newRule.Target = &stack.DNATTarget{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), NetworkProtocol: NetworkProtocolIPv4, ChangeAddress: true, ChangePort: false} // Not totally sure this is the right way to add the prerouting rule, but it seems to work natTable.Rules[stack.Prerouting] = *newRule - ipt.ReplaceTable(stack.NATID, natTable, false) + //ForceReplaceTable ensures IPtables get enabled; ReplaceTable doesn't. + ipt.ForceReplaceTable(stack.NATID, natTable, false) // Make new relay device. diff --git a/src/go.mod b/src/go.mod index f9a2424..dedb08f 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,6 +1,8 @@ module wiretap -go 1.20 +go 1.21.1 + +toolchain go1.23.3 replace golang.zx2c4.com/wireguard => github.com/luker983/wireguard-go v0.0.0-20231019223227-fc689040dc0a @@ -17,7 +19,7 @@ require ( golang.org/x/net v0.23.0 golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb - gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 + gvisor.dev/gvisor v0.0.0-20231115214215-71bcc96c6e38 ) require ( @@ -37,10 +39,10 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/sync v0.3.0 // indirect + golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.1.0 // indirect + golang.org/x/time v0.3.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/src/go.sum b/src/go.sum index f578fb4..5f149fa 100644 --- a/src/go.sum +++ b/src/go.sum @@ -63,6 +63,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -109,6 +110,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -146,9 +148,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= github.com/luker983/wireguard-go v0.0.0-20231019223227-fc689040dc0a h1:gUjN6KFTeYzhwiT2qaf2kRLJhfi5+/yvqxeKYg8vHuA= @@ -175,6 +179,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= @@ -307,8 +312,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -362,8 +367,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -517,8 +522,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +gvisor.dev/gvisor v0.0.0-20231115214215-71bcc96c6e38 h1:iKEOYAMkiugi05kLeJD5v95Fwrm5XP6mstN8RSXD7v4= +gvisor.dev/gvisor v0.0.0-20231115214215-71bcc96c6e38/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 401bf9d32087f265a4223c0affc07782bb525419 Mon Sep 17 00:00:00 2001 From: amerril Date: Wed, 13 Nov 2024 17:49:26 -0700 Subject: [PATCH 03/11] specify localhost IP via serve CLI --- src/cmd/serve.go | 155 ++++++++++++----------------------------------- 1 file changed, 40 insertions(+), 115 deletions(-) diff --git a/src/cmd/serve.go b/src/cmd/serve.go index c3b52c5..49c1a39 100644 --- a/src/cmd/serve.go +++ b/src/cmd/serve.go @@ -23,7 +23,6 @@ import ( gudp "gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip" - //_ "unsafe" //required to use the go:linkname directive "wiretap/peer" "wiretap/transport/api" @@ -50,6 +49,7 @@ type serveCmdConfig struct { keepaliveCount uint keepaliveInterval uint disableV6 bool + localhostIP string } type wiretapDefaultConfig struct { @@ -69,7 +69,7 @@ type wiretapDefaultConfig struct { /* type testMatcher struct { - + } func (t testMatcher) Match(hook stack.Hook, packet stack.PacketBufferPtr, inputInterfaceName, outputInterfaceName string) (matches bool, hotdrop bool) { @@ -83,60 +83,6 @@ func (t testMatcher) Match(hook stack.Hook, packet stack.PacketBufferPtr, inputI } */ -/* -type portOrIdentRange struct { - start uint16 - size uint32 -} - -// Same as stack.DNATTarget, except it forwards ALL ports, not just one. -type DNAT_Target struct { - // The new destination address for packets. - // - // Immutable. - Addr tcpip.Address - - // NetworkProtocol is the network protocol the target is used with. - // - // Immutable. - NetworkProtocol tcpip.NetworkProtocolNumber -} - -// Same as stack.DNATTarget.Action(), except it forwards ALL ports, not just one. -func (rt *DNAT_Target) Action(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, addressEP stack.AddressableEndpoint) (stack.RuleVerdict, int) { - // Sanity check. - if rt.NetworkProtocol != pkt.NetworkProtocolNumber { - panic(fmt.Sprintf( - "DNATTarget.Action with NetworkProtocol %d called on packet with NetworkProtocolNumber %d", - rt.NetworkProtocol, pkt.NetworkProtocolNumber)) - } - - switch hook { - case stack.Prerouting, stack.Output: - case stack.Input, stack.Forward, stack.Postrouting: - panic(fmt.Sprintf("%s not supported for DNAT", hook)) - default: - panic(fmt.Sprintf("%s unrecognized", hook)) - } - - return dnat_Action(pkt, hook, r, rt.Addr) -} - -// Same as the unexported stack.dnatAction(), except it forwards ALL ports by using a large size value, instead of "1". -func dnat_Action(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, address tcpip.Address) (stack.RuleVerdict, int) { - //return stack_natAction(pkt, hook, r, portOrIdentRange{start: 0, size: 65536}, address, true) - fmt.Println("DNAT_action") - return stack_natAction(pkt, hook, r, portOrIdentRange{start: 8000, size: 1}, address, true, false, true) -} - -// Janky unsafe compiler trick to give us access to the unexported stack.natAction() function to make the DNAT stuff work -//go:linkname stack_natAction gvisor.dev/gvisor/pkg/tcpip/stack.natAction -func stack_natAction(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, portsOrIdents portOrIdentRange, address tcpip.Address, dnat, changePort, changeAddress bool) (stack.RuleVerdict, int) - -//old format -//func stack_natAction(pkt stack.PacketBufferPtr, hook stack.Hook, r *stack.Route, portsOrIdents portOrIdentRange, address tcpip.Address, dnat bool) (stack.RuleVerdict, int) -*/ - // Defaults for serve command. var serveCmd = serveCmdConfig{ configFile: "", @@ -155,6 +101,7 @@ var serveCmd = serveCmdConfig{ keepaliveCount: 3, keepaliveInterval: 60, disableV6: false, + localhostIP: "", } var wiretapDefault = wiretapDefaultConfig{ @@ -197,6 +144,7 @@ func init() { cmd.Flags().BoolVarP(&serveCmd.disableV6, "disable-ipv6", "", serveCmd.disableV6, "disable ipv6") cmd.Flags().BoolVarP(&serveCmd.logging, "log", "l", serveCmd.logging, "enable logging to file") cmd.Flags().StringVarP(&serveCmd.logFile, "log-file", "o", serveCmd.logFile, "write log to this filename") + cmd.Flags().StringVarP(&serveCmd.localhostIP, "localhost-ip", "i", serveCmd.localhostIP, "redirect wiretap packets destined for this IPv4 address to server's localhost") cmd.Flags().StringP("api", "0", wiretapDefault.apiAddr, "address of API service") cmd.Flags().IntP("keepalive", "k", wiretapDefault.keepalive, "tunnel keepalive in seconds") cmd.Flags().IntP("mtu", "m", wiretapDefault.mtu, "tunnel MTU") @@ -218,6 +166,9 @@ func init() { err = viper.BindPFlag("disableipv6", cmd.Flags().Lookup("disable-ipv6")) check("error binding flag to viper", err) + err = viper.BindPFlag("localhost-ip", cmd.Flags().Lookup("localhost-ip")) + check("error binding flag to viper", err) + // Quiet and debug flags must be used independently. cmd.MarkFlagsMutuallyExclusive("debug", "quiet") @@ -557,56 +508,41 @@ func (c serveCmdConfig) Run() { } s.SetTransportProtocolHandler(gudp.ProtocolNumber, udp.Handler(udpConfig)) - /* - nics := s.NICInfo() - for k, v := range nics { - fmt.Printf("key %s: %+v \n", k, v) - } - */ - - // Playiung with IPTables - // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20230927004350-cbd86285d259/pkg/tcpip/stack - ipt := s.IPTables() - natTable := ipt.GetTable(stack.NATID, false) - /* All 5 seem to be the same (empty) - fmt.Println("Numnber of rules: ", len(natTable.Rules)) - for i := 0; i < len(natTable.Rules); i++ { - rule := natTable.Rules[i] - //fmt.Println(rule.Filter.InputInterface) - fmt.Printf("%+v\n", rule.Filter) - } - */ - //rule := natTable.Rules[0] - //fmt.Printf("%+v\n", natTable) - - // https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml - NetworkProtocolIPv4 := tcpip.NetworkProtocolNumber(2048) - //NetworkProtocolIPv6 := tcpip.NetworkProtocolNumber(34525) - - newFilter := EmptyFilter4() - newFilter.Dst = tcpip.AddrFromSlice([]byte{192,168,137,137}) - newFilter.DstMask = tcpip.AddrFromSlice([]byte{255,255,255,255}) - - newRule := new(stack.Rule) - newRule.Filter = newFilter - //fmt.Printf("%+v\n", newRule.Filter) - - //Don't need any additional matching conditions besides the IP filter - //newRule.Matchers = []stack.Matcher{testMatcher{}} - - //newRule.Target = &stack.DNATTarget{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), Port: 8000, NetworkProtocol: NetworkProtocolIPv4} - - // gvisor doesn't seem to provide access to the function primitives needed to do DNAT for all ports, so we have to roll our own. - //newRule.Target = &DNAT_Target{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), NetworkProtocol: NetworkProtocolIPv4} - - //Do address-only DNAT; port remains the same, so all ports are effectively forwarded to localhost - newRule.Target = &stack.DNATTarget{Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), NetworkProtocol: NetworkProtocolIPv4, ChangeAddress: true, ChangePort: false} + // Setup localhost forwarding IP using IPTables + // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20231115214215-71bcc96c6e38/pkg/tcpip/stack + if viper.IsSet("localhost-ip") && viper.GetString("localhost-ip") != "" { + localhostAddr, err := netip.ParseAddr(viper.GetString("localhost-ip")) + check("failed to parse localhost-ip address", err) + + // Setup IP filter for localhost re-routing + newFilter := stack.EmptyFilter4() + newFilter.Dst = tcpip.AddrFromSlice(localhostAddr.AsSlice()) + newFilter.DstMask = tcpip.AddrFromSlice([]byte{255,255,255,255}) + + newRule := new(stack.Rule) + newRule.Filter = newFilter + + // https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml + NetworkProtocolIPv4 := tcpip.NetworkProtocolNumber(2048) + //NetworkProtocolIPv6 := tcpip.NetworkProtocolNumber(34525) + + //Do address-only DNAT; port remains the same, so all ports are effectively forwarded to localhost + newRule.Target = &stack.DNATTarget{ + Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), + NetworkProtocol: NetworkProtocolIPv4, + ChangeAddress: true, + ChangePort: false, + } - // Not totally sure this is the right way to add the prerouting rule, but it seems to work - natTable.Rules[stack.Prerouting] = *newRule - //ForceReplaceTable ensures IPtables get enabled; ReplaceTable doesn't. - ipt.ForceReplaceTable(stack.NATID, natTable, false) + // Get the current (blank) IPTables and add the new rule to it + ipt := s.IPTables() + natTable := ipt.GetTable(stack.NATID, false) + // Not 100% sure this is the right way to add the prerouting rule, but it seems to work + natTable.Rules[stack.Prerouting] = *newRule + //ForceReplaceTable ensures IPtables get enabled; ReplaceTable doesn't. + ipt.ForceReplaceTable(stack.NATID, natTable, false) + } // Make new relay device. devRelay := device.NewDevice(tunRelay, conn.NewDefaultBind(), device.NewLogger(logger, "")) @@ -659,14 +595,3 @@ func (c serveCmdConfig) Run() { wg.Wait() } - -// Borrowing this function from a more recent version of the module -// EmptyFilter4 returns an initialized IPv4 header filter. -func EmptyFilter4() stack.IPHeaderFilter { - return stack.IPHeaderFilter{ - Dst: tcpip.AddrFrom4([4]byte{}), - DstMask: tcpip.AddrFrom4([4]byte{}), - Src: tcpip.AddrFrom4([4]byte{}), - SrcMask: tcpip.AddrFrom4([4]byte{}), - } -} From 193825a3240311fa1b9afe5d6aa0b280b840b1bf Mon Sep 17 00:00:00 2001 From: amerril Date: Thu, 14 Nov 2024 18:19:07 -0700 Subject: [PATCH 04/11] add full config support for localhost ip --- src/cmd/configure.go | 46 ++++++++++++++++++++++++---------------- src/cmd/serve.go | 32 ++++++++++++++++++++-------- src/peer/config.go | 50 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 90 insertions(+), 38 deletions(-) diff --git a/src/cmd/configure.go b/src/cmd/configure.go index 78d6b3d..f4a76c5 100644 --- a/src/cmd/configure.go +++ b/src/cmd/configure.go @@ -35,6 +35,7 @@ type configureCmdConfig struct { keepalive int mtu int disableV6 bool + localhostIP string } // Defaults for configure command. @@ -61,6 +62,7 @@ var configureCmdArgs = configureCmdConfig{ keepalive: Keepalive, mtu: MTU, disableV6: false, + localhostIP: "", } // configureCmd represents the configure command. @@ -82,7 +84,8 @@ func init() { configureCmd.Flags().BoolVar(&configureCmdArgs.outbound, "outbound", configureCmdArgs.outbound, "client will initiate handshake to server; --endpoint now specifies server's listening socket instead of client's, and --port assigns the server's listening port instead of client's") configureCmd.Flags().IntVarP(&configureCmdArgs.port, "port", "p", configureCmdArgs.port, "listener port for wireguard relay. Default is to copy the --endpoint port. If --outbound, sets port for the server; else for the client.") configureCmd.Flags().StringVarP(&configureCmdArgs.nickname, "nickname", "n", configureCmdArgs.nickname, "Server nickname to display in 'status' command") - + configureCmd.Flags().StringVarP(&configureCmdArgs.localhostIP, "localhost-ip", "l", configureCmdArgs.localhostIP, "Redirect wiretap packets destined for this IPv4 address to server's localhost") + configureCmd.Flags().StringVarP(&configureCmdArgs.configFileRelay, "relay-output", "", configureCmdArgs.configFileRelay, "wireguard relay config output filename") configureCmd.Flags().StringVarP(&configureCmdArgs.configFileE2EE, "e2ee-output", "", configureCmdArgs.configFileE2EE, "wireguard E2EE config output filename") configureCmd.Flags().StringVarP(&configureCmdArgs.configFileServer, "server-output", "s", configureCmdArgs.configFileServer, "wiretap server config output filename") @@ -93,7 +96,7 @@ func init() { configureCmd.Flags().IntVarP(&configureCmdArgs.keepalive, "keepalive", "k", configureCmdArgs.keepalive, "tunnel keepalive in seconds, only applies to outbound handshakes") configureCmd.Flags().IntVarP(&configureCmdArgs.mtu, "mtu", "m", configureCmdArgs.mtu, "tunnel MTU") configureCmd.Flags().BoolVarP(&configureCmdArgs.disableV6, "disable-ipv6", "", configureCmdArgs.disableV6, "disables IPv6") - + configureCmd.Flags().StringVarP(&configureCmdArgs.clientAddr4Relay, "ipv4-relay", "", configureCmdArgs.clientAddr4Relay, "ipv4 relay address") configureCmd.Flags().StringVarP(&configureCmdArgs.clientAddr6Relay, "ipv6-relay", "", configureCmdArgs.clientAddr6Relay, "ipv6 relay address") configureCmd.Flags().StringVarP(&configureCmdArgs.clientAddr4E2EE, "ipv4-e2ee", "", configureCmdArgs.clientAddr4E2EE, "ipv4 e2ee address") @@ -112,15 +115,15 @@ func init() { configureCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { if !ShowHidden { for _, f := range []string{ - "api", - "ipv4-relay", - "ipv6-relay", - "ipv4-e2ee", - "ipv6-e2ee", - "ipv4-relay-server", - "ipv6-relay-server", - "keepalive", - "mtu", + "api", + "ipv4-relay", + "ipv6-relay", + "ipv4-e2ee", + "ipv6-e2ee", + "ipv4-relay-server", + "ipv6-relay-server", + "keepalive", + "mtu", "disable-ipv6", "relay-output", "e2ee-output", @@ -141,6 +144,9 @@ func init() { func (c configureCmdConfig) Run() { var err error + if c.localhostIP != "" { + c.allowedIPs = append(c.allowedIPs, c.localhostIP+"/32") + } if c.disableV6 && netip.MustParsePrefix(c.apiAddr).Addr().Is6() { c.apiAddr = c.apiv4Addr } @@ -177,16 +183,16 @@ func (c configureCmdConfig) Run() { if !c.disableV6 { clientE2EEAddrs = append(clientE2EEAddrs, c.clientAddr6E2EE) } - + if c.port == USE_ENDPOINT_PORT { c.port = portFromEndpoint(c.endpoint) } - + // We only configure one of these (based on --outbound or not) // The other must be manually changed in the configs/command/envs - var clientPort int; - var serverPort int; - + var clientPort int + var serverPort int + if c.outbound { clientPort = Port serverPort = c.port @@ -194,7 +200,7 @@ func (c configureCmdConfig) Run() { clientPort = c.port serverPort = Port } - + err = serverConfigRelay.SetPort(serverPort) check("failed to set port", err) @@ -242,7 +248,7 @@ func (c configureCmdConfig) Run() { PublicKey: serverConfigE2EE.GetPublicKey(), AllowedIPs: c.allowedIPs, Endpoint: net.JoinHostPort(relaySubnet4.Addr().Next().Next().String(), fmt.Sprint(E2EEPort)), - Nickname: c.nickname, + Nickname: c.nickname, }, }, Addresses: clientE2EEAddrs, @@ -277,6 +283,10 @@ func (c configureCmdConfig) Run() { err = serverConfigRelay.SetMTU(c.mtu) check("failed to set mtu", err) } + if c.localhostIP != "" { + err = serverConfigRelay.SetLocalhostIP(c.localhostIP) + check("failed to set localhost IP", err) + } // Add number to filename if it already exists. c.configFileRelay = peer.FindAvailableFilename(c.configFileRelay) diff --git a/src/cmd/serve.go b/src/cmd/serve.go index 49c1a39..dc3b23c 100644 --- a/src/cmd/serve.go +++ b/src/cmd/serve.go @@ -17,12 +17,12 @@ import ( "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun/netstack" + "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" gtcp "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" gudp "gvisor.dev/gvisor/pkg/tcpip/transport/udp" - "gvisor.dev/gvisor/pkg/tcpip/stack" - "gvisor.dev/gvisor/pkg/tcpip" "wiretap/peer" "wiretap/transport/api" @@ -166,7 +166,7 @@ func init() { err = viper.BindPFlag("disableipv6", cmd.Flags().Lookup("disable-ipv6")) check("error binding flag to viper", err) - err = viper.BindPFlag("localhost-ip", cmd.Flags().Lookup("localhost-ip")) + err = viper.BindPFlag("Relay.Interface.LocalhostIP", cmd.Flags().Lookup("localhost-ip")) check("error binding flag to viper", err) // Quiet and debug flags must be used independently. @@ -510,14 +510,15 @@ func (c serveCmdConfig) Run() { // Setup localhost forwarding IP using IPTables // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20231115214215-71bcc96c6e38/pkg/tcpip/stack - if viper.IsSet("localhost-ip") && viper.GetString("localhost-ip") != "" { - localhostAddr, err := netip.ParseAddr(viper.GetString("localhost-ip")) + //if viper.IsSet("localhostip") && viper.GetString("localhostip") != "" { + if viper.IsSet("Relay.Interface.LocalhostIP") && viper.GetString("Relay.Interface.LocalhostIP") != "" { + localhostAddr, err := netip.ParseAddr(viper.GetString("Relay.Interface.LocalhostIP")) check("failed to parse localhost-ip address", err) // Setup IP filter for localhost re-routing newFilter := stack.EmptyFilter4() newFilter.Dst = tcpip.AddrFromSlice(localhostAddr.AsSlice()) - newFilter.DstMask = tcpip.AddrFromSlice([]byte{255,255,255,255}) + newFilter.DstMask = tcpip.AddrFromSlice([]byte{255, 255, 255, 255}) newRule := new(stack.Rule) newRule.Filter = newFilter @@ -528,10 +529,10 @@ func (c serveCmdConfig) Run() { //Do address-only DNAT; port remains the same, so all ports are effectively forwarded to localhost newRule.Target = &stack.DNATTarget{ - Addr: tcpip.AddrFromSlice([]byte{127,0,0,1}), + Addr: tcpip.AddrFromSlice([]byte{127, 0, 0, 1}), NetworkProtocol: NetworkProtocolIPv4, - ChangeAddress: true, - ChangePort: false, + ChangeAddress: true, + ChangePort: false, } // Get the current (blank) IPTables and add the new rule to it @@ -542,6 +543,19 @@ func (c serveCmdConfig) Run() { //ForceReplaceTable ensures IPtables get enabled; ReplaceTable doesn't. ipt.ForceReplaceTable(stack.NATID, natTable, false) + + if localhostAddr.IsLoopback() { + fmt.Printf("=== WARNING: %s is a loopback IP. It will probably not work for Localhost Forwarding ===\n", localhostAddr.String()) + + } else if localhostAddr.IsMulticast() { + fmt.Printf("=== WARNING: %s is a Multicast IP. Your OS might still send extra packets to other IPs when you target this IP ===\n", localhostAddr.String()) + + } else if !localhostAddr.IsPrivate() { + fmt.Printf("=== WARNING: %s is a public IP. If Localhost Forwarding fails, your traffic may actually touch that IP ===\n", localhostAddr.String()) + } + + fmt.Println("Localhost Forwarding configured for ", localhostAddr) + fmt.Println() } // Make new relay device. diff --git a/src/peer/config.go b/src/peer/config.go index cf83d72..3f3b1a6 100644 --- a/src/peer/config.go +++ b/src/peer/config.go @@ -14,17 +14,19 @@ import ( ) type Config struct { - config wgtypes.Config - mtu int - peers []PeerConfig - addresses []net.IPNet + config wgtypes.Config + mtu int + peers []PeerConfig + addresses []net.IPNet + localhostIP string } type configJSON struct { - Config wgtypes.Config - MTU int - Peers []PeerConfig - Addresses []net.IPNet + Config wgtypes.Config + MTU int + Peers []PeerConfig + Addresses []net.IPNet + LocalhostIP string } type ConfigArgs struct { @@ -35,6 +37,7 @@ type ConfigArgs struct { ReplacePeers bool Peers []PeerConfigArgs Addresses []string + LocalhostIP string } type Shell uint @@ -152,6 +155,9 @@ func ParseConfig(filename string) (c Config, err error) { return c, e } err = c.SetMTU(mtu) + case "localhostip": + err = c.SetLocalhostIP(value) + fmt.Println("LocalhostIP value parsed") } if err != nil { return c, err @@ -163,13 +169,13 @@ func ParseConfig(filename string) (c Config, err error) { if len(line) == 0 { continue } - + if strings.HasPrefix(line, CUSTOM_PREFIX) { //special wiretap-specific values line = line[len(CUSTOM_PREFIX):] } else if line[0] == '#' { continue } - + key, value, err := parseConfigLine(line) if err != nil { return c, err @@ -220,6 +226,7 @@ func (c *Config) MarshalJSON() ([]byte, error) { c.mtu, c.peers, c.addresses, + c.localhostIP, }) } @@ -375,6 +382,15 @@ func (c *Config) GetPeerEndpoint(i int) string { return "" } +func (c *Config) GetLocalhostIP() string { + return c.localhostIP +} + +func (c *Config) SetLocalhostIP(ip string) error { + c.localhostIP = ip + return nil +} + // Convert config to peer config, only transfers keys. func (c *Config) AsPeer() (p PeerConfig, err error) { p, err = NewPeerConfig() @@ -401,6 +417,9 @@ func (c *Config) AsFile() string { if c.mtu != 0 { s.WriteString(fmt.Sprintf("MTU = %d\n", c.mtu)) } + if c.localhostIP != "" { + s.WriteString(fmt.Sprintf("LocalhostIP = %s\n", c.localhostIP)) + } for _, p := range c.peers { s.WriteString(fmt.Sprintf("\n%s", p.AsFile())) } @@ -414,7 +433,7 @@ func (c *Config) AsShareableFile() string { s.WriteString("[Peer]\n") s.WriteString(fmt.Sprintf("PublicKey = %s\n", c.config.PrivateKey.PublicKey().String())) s.WriteString("AllowedIPs = 0.0.0.0/32\n") - + return s.String() } @@ -506,6 +525,11 @@ func CreateServerCommand(relayConfig Config, e2eeConfig Config, shell Shell, sim vals = append(vals, "true") } + if len(relayConfig.GetLocalhostIP()) > 0 { + keys = append(keys, "WIRETAP_RELAY_INTERFACE_LOCALHOSTIP") + vals = append(vals, relayConfig.GetLocalhostIP()) + } + switch shell { case POSIX: for i := 0; i < len(keys); i++ { @@ -544,6 +568,10 @@ func CreateServerFile(relayConfig Config, e2eeConfig Config) string { s.WriteString(fmt.Sprintf("MTU = %d\n", relayConfig.mtu)) } + if relayConfig.localhostIP != "" { + s.WriteString(fmt.Sprintf("LocalhostIP = %s\n", relayConfig.GetLocalhostIP())) + } + // Relay Peer. s.WriteString("\n[Relay.Peer]\n") From d6e04109660333aa7d51f264cb183667df1d2748 Mon Sep 17 00:00:00 2001 From: amerril Date: Tue, 19 Nov 2024 13:56:01 -0700 Subject: [PATCH 05/11] fix API proxy behavior --- src/api/api.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/api.go b/src/api/api.go index e1d85ef..fafc002 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -28,7 +28,11 @@ type request struct { // MakeRequest attempts to send an API query to the Wiretap server. func makeRequest(req request) ([]byte, error) { - client := &http.Client{Timeout: 3 * time.Second} + // Never use a proxy for API requests, they must go direct through the Wiretap network + tr := &http.Transport{ + Proxy: nil, + } + client := &http.Client{Timeout: 3 * time.Second, Transport: tr} reqBody := bytes.NewBuffer(req.Body) r, err := http.NewRequest(req.Method, req.URL, reqBody) From ab4cddb6fedc1b9f33a455c3fb7e994d7be8b770 Mon Sep 17 00:00:00 2001 From: amerril Date: Tue, 19 Nov 2024 14:08:19 -0700 Subject: [PATCH 06/11] localhost IP support for 'add server' --- src/cmd/add_server.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/cmd/add_server.go b/src/cmd/add_server.go index e9919f9..7364372 100644 --- a/src/cmd/add_server.go +++ b/src/cmd/add_server.go @@ -26,6 +26,7 @@ type addServerCmdConfig struct { writeToClipboard bool port int nickname string + localhostIP string } var addServerCmdArgs = addServerCmdConfig{ @@ -37,6 +38,7 @@ var addServerCmdArgs = addServerCmdConfig{ writeToClipboard: false, port: USE_ENDPOINT_PORT, nickname: "", + localhostIP: "", } // addServerCmd represents the server command. @@ -56,8 +58,9 @@ func init() { addServerCmd.Flags().StringVarP(&addServerCmdArgs.serverAddress, "server-address", "s", addServerCmdArgs.serverAddress, "API address of server that new server will connect to, connects to client by default") addServerCmd.Flags().IntVarP(&addServerCmdArgs.port, "port", "p", addServerCmdArgs.port, "listener port to start on new server for wireguard relay. If --outbound, default is the port specified in --endpoint; otherwise default is 51820") addServerCmd.Flags().StringVarP(&addServerCmdArgs.nickname, "nickname", "n", addServerCmdArgs.nickname, "Server nickname to display in 'status' command") + addServerCmd.Flags().StringVarP(&addServerCmdArgs.localhostIP, "localhost-ip", "i", addServerCmdArgs.localhostIP, "[EXPERIMENTAL] Redirect wiretap packets destined for this IPv4 address to server's localhost") addServerCmd.Flags().BoolVarP(&addServerCmdArgs.writeToClipboard, "clipboard", "c", addServerCmdArgs.writeToClipboard, "copy configuration args to clipboard") - + addServerCmd.Flags().StringVarP(&addServerCmdArgs.configFileRelay, "relay-input", "", addServerCmdArgs.configFileRelay, "filename of input relay config file") addServerCmd.Flags().StringVarP(&addServerCmdArgs.configFileE2EE, "e2ee-input", "", addServerCmdArgs.configFileE2EE, "filename of input E2EE config file") addServerCmd.Flags().StringVarP(&addServerCmdArgs.configFileServer, "server-output", "", addServerCmdArgs.configFileServer, "filename of server config output file") @@ -67,7 +70,7 @@ func init() { addServerCmd.Flags().SortFlags = false addServerCmd.PersistentFlags().SortFlags = false - + helpFunc := addCmd.HelpFunc() addCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { if !ShowHidden { @@ -83,7 +86,7 @@ func init() { } else { fmt.Printf("Failed to hide flag %v: %v\n", f, err) } - + } } } @@ -170,7 +173,7 @@ func (c addServerCmdConfig) Run() { PublicKey: serverConfigE2EE.GetPublicKey(), AllowedIPs: c.allowedIPs, Endpoint: net.JoinHostPort(newRelayPrefixes[0].Addr().Next().Next().String(), fmt.Sprint(E2EEPort)), - Nickname: c.nickname, + Nickname: c.nickname, }) check("failed to generate new e2ee peer", err) clientConfigE2EE.AddPeer(serverE2EEPeer) @@ -264,7 +267,7 @@ func (c addServerCmdConfig) Run() { PublicKey: serverConfigE2EE.GetPublicKey(), AllowedIPs: c.allowedIPs, Endpoint: net.JoinHostPort(addresses.NextServerRelayAddr4.String(), fmt.Sprint(E2EEPort)), - Nickname: c.nickname, + Nickname: c.nickname, }) check("failed to parse server as peer", err) clientConfigE2EE.AddPeer(serverPeerConfigE2EE) @@ -336,20 +339,24 @@ func (c addServerCmdConfig) Run() { // Leaf server is the relay peer for the new server. clientConfigRelay = leafServerConfigRelay } - + // Set port defaults if c.port == USE_ENDPOINT_PORT { if addArgs.outbound { //for outbound, default port is same as endpoint port c.port = portFromEndpoint(addArgs.endpoint) - + } else { //for inbound, use a reasonable default for server relay listening port - c.port = Port; + c.port = Port } } - + err = serverConfigRelay.SetPort(c.port) check("failed to set port", err) + // Setup localhost IP relay + err = serverConfigRelay.SetLocalhostIP(c.localhostIP) + check("failed to set localhost IP", err) + // Overwrite Relay file with new server peer if adding a server directly to the client. var fileStatusRelay string if len(c.serverAddress) == 0 { From 33d1ee5726b7be529c0ccc1f63852c13945a467f Mon Sep 17 00:00:00 2001 From: amerril Date: Tue, 19 Nov 2024 14:10:08 -0700 Subject: [PATCH 07/11] code cleanup; add feature to README --- README.md | 17 +++++++++++++ src/cmd/configure.go | 2 +- src/cmd/serve.go | 57 +++++++++++++++++++++++++++++--------------- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 9816f42..950e756 100644 --- a/README.md +++ b/README.md @@ -539,6 +539,23 @@ Please see the [Demo page in the Wiki](https://github.com/sandialabs/wiretap/wik # Experimental +## Localhost Server Access + +Sometimes you want to access many ports on the Server itself that are listening on the localhost/loopback interface instead of a public interface. Rather than setting up many individual port forwards, you can use Wiretap's "localhost IP" redirection feature. + +When running the `configure` or `add server` commands, you can specify a `--localhost-ip ` argument. Any packets received by this Server through the Wiretap network with this target destination address will be rerouted to the Server host's `127.0.0.1` loopback address instead, with replies routed back to the client appropriately. + +> [!CAUTION] +> It is **strongly** recommended that you specify a private (non-routable) IP address to use for this option, preferably one that you know is not in use in the target network. This feature has only been lightly tested, so if the re-routing fails unexpectedly you want to ensure your traffic will go to a "safe" destination. For similar reasons you should not specify a broadcast address, or IPs that your Client already has routes for. + +Under the hood, this argument is roughly equivalent to adding this `iptables` rule to Wiretap's userspace networking stack on the Server: +``` +iptables -t nat -A PREROUTING -p tcp -d -j DNAT --to-destination 127.0.0.1 +``` + +Currently this only works for TCP connections, and only for an IPv4 target address. Unfortunately there's [not a clean way](https://serverfault.com/a/975890) to do NAT to the IPv6 `::1` loopback address, so this feature can't be used to access services listening exclusively on that IPv6 address. + + ## TCP Tunneling > [!WARNING] diff --git a/src/cmd/configure.go b/src/cmd/configure.go index f4a76c5..c35e0a0 100644 --- a/src/cmd/configure.go +++ b/src/cmd/configure.go @@ -84,7 +84,7 @@ func init() { configureCmd.Flags().BoolVar(&configureCmdArgs.outbound, "outbound", configureCmdArgs.outbound, "client will initiate handshake to server; --endpoint now specifies server's listening socket instead of client's, and --port assigns the server's listening port instead of client's") configureCmd.Flags().IntVarP(&configureCmdArgs.port, "port", "p", configureCmdArgs.port, "listener port for wireguard relay. Default is to copy the --endpoint port. If --outbound, sets port for the server; else for the client.") configureCmd.Flags().StringVarP(&configureCmdArgs.nickname, "nickname", "n", configureCmdArgs.nickname, "Server nickname to display in 'status' command") - configureCmd.Flags().StringVarP(&configureCmdArgs.localhostIP, "localhost-ip", "l", configureCmdArgs.localhostIP, "Redirect wiretap packets destined for this IPv4 address to server's localhost") + configureCmd.Flags().StringVarP(&configureCmdArgs.localhostIP, "localhost-ip", "i", configureCmdArgs.localhostIP, "[EXPERIMENTAL] Redirect wiretap packets destined for this IPv4 address to server's localhost") configureCmd.Flags().StringVarP(&configureCmdArgs.configFileRelay, "relay-output", "", configureCmdArgs.configFileRelay, "wireguard relay config output filename") configureCmd.Flags().StringVarP(&configureCmdArgs.configFileE2EE, "e2ee-output", "", configureCmdArgs.configFileE2EE, "wireguard E2EE config output filename") diff --git a/src/cmd/serve.go b/src/cmd/serve.go index dc3b23c..9b9787f 100644 --- a/src/cmd/serve.go +++ b/src/cmd/serve.go @@ -67,22 +67,6 @@ type wiretapDefaultConfig struct { mtu int } -/* -type testMatcher struct { - -} - -func (t testMatcher) Match(hook stack.Hook, packet stack.PacketBufferPtr, inputInterfaceName, outputInterfaceName string) (matches bool, hotdrop bool) { - fmt.Println("Checking for match") - if hook == stack.Prerouting { - fmt.Println("Match!") - return true, false - } - fmt.Println("No match") - return false, false -} -*/ - // Defaults for serve command. var serveCmd = serveCmdConfig{ configFile: "", @@ -144,7 +128,7 @@ func init() { cmd.Flags().BoolVarP(&serveCmd.disableV6, "disable-ipv6", "", serveCmd.disableV6, "disable ipv6") cmd.Flags().BoolVarP(&serveCmd.logging, "log", "l", serveCmd.logging, "enable logging to file") cmd.Flags().StringVarP(&serveCmd.logFile, "log-file", "o", serveCmd.logFile, "write log to this filename") - cmd.Flags().StringVarP(&serveCmd.localhostIP, "localhost-ip", "i", serveCmd.localhostIP, "redirect wiretap packets destined for this IPv4 address to server's localhost") + cmd.Flags().StringVarP(&serveCmd.localhostIP, "localhost-ip", "i", serveCmd.localhostIP, "[EXPERIMENTAL] redirect Wiretap packets destined for this IPv4 address to server's localhost") cmd.Flags().StringP("api", "0", wiretapDefault.apiAddr, "address of API service") cmd.Flags().IntP("keepalive", "k", wiretapDefault.keepalive, "tunnel keepalive in seconds") cmd.Flags().IntP("mtu", "m", wiretapDefault.mtu, "tunnel MTU") @@ -510,7 +494,6 @@ func (c serveCmdConfig) Run() { // Setup localhost forwarding IP using IPTables // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20231115214215-71bcc96c6e38/pkg/tcpip/stack - //if viper.IsSet("localhostip") && viper.GetString("localhostip") != "" { if viper.IsSet("Relay.Interface.LocalhostIP") && viper.GetString("Relay.Interface.LocalhostIP") != "" { localhostAddr, err := netip.ParseAddr(viper.GetString("Relay.Interface.LocalhostIP")) check("failed to parse localhost-ip address", err) @@ -525,7 +508,6 @@ func (c serveCmdConfig) Run() { // https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml NetworkProtocolIPv4 := tcpip.NetworkProtocolNumber(2048) - //NetworkProtocolIPv6 := tcpip.NetworkProtocolNumber(34525) //Do address-only DNAT; port remains the same, so all ports are effectively forwarded to localhost newRule.Target = &stack.DNATTarget{ @@ -541,6 +523,43 @@ func (c serveCmdConfig) Run() { // Not 100% sure this is the right way to add the prerouting rule, but it seems to work natTable.Rules[stack.Prerouting] = *newRule + /* Example of how to add another rule to a chain + // Setup IPv6 filter for localhost re-routing + i6Filter := stack.EmptyFilter4() + i6Filter.Dst = tcpip.AddrFromSlice([]byte{0xfd, 0x90, 0x13, 0x37, 0x13, 0x37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) + fmt.Println(i6Filter.Dst.String()) + i6Filter.DstMask = tcpip.AddrFromSlice([]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}) + + i6Rule := new(stack.Rule) + i6Rule.Filter = i6Filter + + NetworkProtocolIPv6 := tcpip.NetworkProtocolNumber(34525) + // This fails because apparently routing to ::1 isn't allowed. https://serverfault.com/questions/1122125/why-does-ip6tables-lose-packets-after-prerouting-to-a-different-interface + i6Rule.Target = &stack.DNATTarget{ + //Addr: tcpip.AddrFromSlice([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), + Addr: tcpip.AddrFromSlice([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1}), + NetworkProtocol: NetworkProtocolIPv6, + ChangeAddress: true, + ChangePort: false, + } + + //Add the IPv6 rule, and fix up the associated chains and underflows + natTable.Rules = slices.Insert(natTable.Rules, int(stack.Prerouting)+1, *i6Rule) + + for hook, ruleIndex := range natTable.BuiltinChains { + if hook != int(stack.Prerouting) && ruleIndex > 0 { + natTable.BuiltinChains[hook] = ruleIndex + 1 + } + } + for hook, ruleIndex := range natTable.Underflows { + if hook != int(stack.Prerouting) && ruleIndex > 0 { + natTable.Underflows[hook] = ruleIndex + 1 + } + } + + fmt.Printf("%+v\n", natTable) + */ + //ForceReplaceTable ensures IPtables get enabled; ReplaceTable doesn't. ipt.ForceReplaceTable(stack.NATID, natTable, false) From 6443c0113c5171ff203dce0eb904c351bede37bf Mon Sep 17 00:00:00 2001 From: amerril Date: Tue, 19 Nov 2024 15:14:19 -0700 Subject: [PATCH 08/11] README improvements --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 950e756..7d9971d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ In this diagram, the Client has generated and installed WireGuard configuration - [Features](#features) - [Demo](#demo) - [Experimental](#experimental) + - [Localhost Server Access](#localhost-server-access) - [TCP Tunneling](#tcp-tunneling) - [Add Clients To Any Server](#add-clients-to-any-server) @@ -543,17 +544,24 @@ Please see the [Demo page in the Wiki](https://github.com/sandialabs/wiretap/wik Sometimes you want to access many ports on the Server itself that are listening on the localhost/loopback interface instead of a public interface. Rather than setting up many individual port forwards, you can use Wiretap's "localhost IP" redirection feature. -When running the `configure` or `add server` commands, you can specify a `--localhost-ip ` argument. Any packets received by this Server through the Wiretap network with this target destination address will be rerouted to the Server host's `127.0.0.1` loopback address instead, with replies routed back to the client appropriately. +When running the `configure` or `add server` commands, you can specify a `--localhost-ip ` argument. For example: +```bash +./wiretap configure --endpoint 7.3.3.1:1337 --routes 10.0.0.0/24 -i 192.168.137.137 +``` +Any packets received by this Server through the Wiretap network with this target destination address (`192.168.137.137` in this example) will be rerouted to the Server host's `127.0.0.1` loopback address instead, with replies routed back to the Client appropriately. > [!CAUTION] > It is **strongly** recommended that you specify a private (non-routable) IP address to use for this option, preferably one that you know is not in use in the target network. This feature has only been lightly tested, so if the re-routing fails unexpectedly you want to ensure your traffic will go to a "safe" destination. For similar reasons you should not specify a broadcast address, or IPs that your Client already has routes for. -Under the hood, this argument is roughly equivalent to adding this `iptables` rule to Wiretap's userspace networking stack on the Server: +Under the hood, this feature is roughly equivalent to adding this `iptables` rule to Wiretap's userspace networking stack on the Server: ``` iptables -t nat -A PREROUTING -p tcp -d -j DNAT --to-destination 127.0.0.1 ``` -Currently this only works for TCP connections, and only for an IPv4 target address. Unfortunately there's [not a clean way](https://serverfault.com/a/975890) to do NAT to the IPv6 `::1` loopback address, so this feature can't be used to access services listening exclusively on that IPv6 address. +Limitations: +- Currently this only works for TCP connections, and only for an IPv4 target address. + - Unfortunately there's [not a clean way](https://serverfault.com/a/975890) to do NAT to the IPv6 `::1` loopback address, so this feature can't be used to access services listening exclusively on that IPv6 address. +- This feature does not provide access to other IPs in the 127.0.0.0/8 space. ## TCP Tunneling From 951e4b5f17970f0a13816c274b07c1d0fb66b63b Mon Sep 17 00:00:00 2001 From: amerril Date: Tue, 19 Nov 2024 15:59:26 -0700 Subject: [PATCH 09/11] update workflows for newer golang version --- .github/workflows/golangci-lint.yml | 8 ++++---- .github/workflows/goreleaser.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5597def..0e89c75 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -15,12 +15,12 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: "1.20" + go-version: "1.23.3" - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version # version: latest diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 69f69db..9bd8118 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.20" + go-version: "1.23.3" cache: true cache-dependency-path: src/go.sum - name: Check GoReleaser Config From f617d1b4f02b397afa1f83823e80863373736454 Mon Sep 17 00:00:00 2001 From: amerril Date: Tue, 19 Nov 2024 16:03:56 -0700 Subject: [PATCH 10/11] update required golang version in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d9971d..ce4af54 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ See the [Usage section](#Usage) for more details. No installation of Wiretap is required. Just grab a binary from the [releases](https://github.com/sandialabs/wiretap/releases) page. You may need two different binaries if the OS/ARCH are different on the client and server machines. -If you want to compile it yourself or can't find the OS/ARCH you're looking for, install Go (>=1.20) from https://go.dev/dl/ and use the provided [Makefile](./src/Makefile). +If you want to compile it yourself or can't find the OS/ARCH you're looking for, install Go (>=1.23.3) from https://go.dev/dl/ and use the provided [Makefile](./src/Makefile). # How it Works From 398dcb013938c9352ea17cc2ea6fe2d7ac58714e Mon Sep 17 00:00:00 2001 From: amerril Date: Thu, 21 Nov 2024 14:42:31 -0700 Subject: [PATCH 11/11] cleaner, more robust iptables code --- src/cmd/serve.go | 118 ++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/src/cmd/serve.go b/src/cmd/serve.go index 9b9787f..35f4c46 100644 --- a/src/cmd/serve.go +++ b/src/cmd/serve.go @@ -10,6 +10,7 @@ import ( "strings" "sync" "time" + "slices" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -493,75 +494,14 @@ func (c serveCmdConfig) Run() { s.SetTransportProtocolHandler(gudp.ProtocolNumber, udp.Handler(udpConfig)) // Setup localhost forwarding IP using IPTables - // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20231115214215-71bcc96c6e38/pkg/tcpip/stack if viper.IsSet("Relay.Interface.LocalhostIP") && viper.GetString("Relay.Interface.LocalhostIP") != "" { localhostAddr, err := netip.ParseAddr(viper.GetString("Relay.Interface.LocalhostIP")) check("failed to parse localhost-ip address", err) - - // Setup IP filter for localhost re-routing - newFilter := stack.EmptyFilter4() - newFilter.Dst = tcpip.AddrFromSlice(localhostAddr.AsSlice()) - newFilter.DstMask = tcpip.AddrFromSlice([]byte{255, 255, 255, 255}) - - newRule := new(stack.Rule) - newRule.Filter = newFilter - - // https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml - NetworkProtocolIPv4 := tcpip.NetworkProtocolNumber(2048) - - //Do address-only DNAT; port remains the same, so all ports are effectively forwarded to localhost - newRule.Target = &stack.DNATTarget{ - Addr: tcpip.AddrFromSlice([]byte{127, 0, 0, 1}), - NetworkProtocol: NetworkProtocolIPv4, - ChangeAddress: true, - ChangePort: false, - } - - // Get the current (blank) IPTables and add the new rule to it - ipt := s.IPTables() - natTable := ipt.GetTable(stack.NATID, false) - // Not 100% sure this is the right way to add the prerouting rule, but it seems to work - natTable.Rules[stack.Prerouting] = *newRule - - /* Example of how to add another rule to a chain - // Setup IPv6 filter for localhost re-routing - i6Filter := stack.EmptyFilter4() - i6Filter.Dst = tcpip.AddrFromSlice([]byte{0xfd, 0x90, 0x13, 0x37, 0x13, 0x37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) - fmt.Println(i6Filter.Dst.String()) - i6Filter.DstMask = tcpip.AddrFromSlice([]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}) - - i6Rule := new(stack.Rule) - i6Rule.Filter = i6Filter - - NetworkProtocolIPv6 := tcpip.NetworkProtocolNumber(34525) - // This fails because apparently routing to ::1 isn't allowed. https://serverfault.com/questions/1122125/why-does-ip6tables-lose-packets-after-prerouting-to-a-different-interface - i6Rule.Target = &stack.DNATTarget{ - //Addr: tcpip.AddrFromSlice([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), - Addr: tcpip.AddrFromSlice([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1}), - NetworkProtocol: NetworkProtocolIPv6, - ChangeAddress: true, - ChangePort: false, - } - - //Add the IPv6 rule, and fix up the associated chains and underflows - natTable.Rules = slices.Insert(natTable.Rules, int(stack.Prerouting)+1, *i6Rule) - - for hook, ruleIndex := range natTable.BuiltinChains { - if hook != int(stack.Prerouting) && ruleIndex > 0 { - natTable.BuiltinChains[hook] = ruleIndex + 1 - } - } - for hook, ruleIndex := range natTable.Underflows { - if hook != int(stack.Prerouting) && ruleIndex > 0 { - natTable.Underflows[hook] = ruleIndex + 1 - } + if len(localhostAddr.AsSlice()) != 4 { + log.Fatalf("Localhost IP must be an IPv4 address") } - fmt.Printf("%+v\n", natTable) - */ - - //ForceReplaceTable ensures IPtables get enabled; ReplaceTable doesn't. - ipt.ForceReplaceTable(stack.NATID, natTable, false) + configureLocalhostForwarding(localhostAddr, s) if localhostAddr.IsLoopback() { fmt.Printf("=== WARNING: %s is a loopback IP. It will probably not work for Localhost Forwarding ===\n", localhostAddr.String()) @@ -628,3 +568,53 @@ func (c serveCmdConfig) Run() { wg.Wait() } + +// Setup iptables rule for localhost re-routing (DNAT) +func configureLocalhostForwarding(localhostAddr netip.Addr, s *stack.Stack) { + // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20231115214215-71bcc96c6e38/pkg/tcpip/stack + newFilter := stack.EmptyFilter4() + newFilter.Dst = tcpip.AddrFromSlice(localhostAddr.AsSlice()) + newFilter.DstMask = tcpip.AddrFromSlice([]byte{255, 255, 255, 255}) + + newRule := new(stack.Rule) + newRule.Filter = newFilter + + //Do address-only DNAT; port remains the same, so all ports are effectively forwarded to localhost + newRule.Target = &stack.DNATTarget{ + Addr: tcpip.AddrFromSlice([]byte{127, 0, 0, 1}), + NetworkProtocol: ipv4.ProtocolNumber, + ChangeAddress: true, + ChangePort: false, + } + + ipt := s.IPTables() + natTable := ipt.GetTable(stack.NATID, false) + newTable := prependIPtableRule(natTable, *newRule, stack.Prerouting) + + //ForceReplaceTable ensures IPtables get enabled; ReplaceTable doesn't. + ipt.ForceReplaceTable(stack.NATID, newTable, false) +} + +// Adds a rule to the start of a table chain. +func prependIPtableRule(table stack.Table, newRule stack.Rule, chain stack.Hook) (stack.Table) { + insertIndex := int(table.BuiltinChains[chain]) + fmt.Printf("Inserting rule into index %d\n", insertIndex) + table.Rules = slices.Insert(table.Rules, insertIndex, newRule) + + // Increment the later chain and underflow index pointers to account for the rule added to the Rules slice + // https://pkg.go.dev/gvisor.dev/gvisor@v0.0.0-20231115214215-71bcc96c6e38/pkg/tcpip/stack#Table + for chainHook, ruleIndex := range table.BuiltinChains { + //assumes each chain has its own unique starting rule index + if ruleIndex > insertIndex { + table.BuiltinChains[chainHook] = ruleIndex + 1 + + } + } + for chainHook, ruleIndex := range table.Underflows { + if ruleIndex >= insertIndex { + table.Underflows[chainHook] = ruleIndex + 1 + } + } + + return table +} \ No newline at end of file