Skip to content

Commit

Permalink
introduced new dns over ssh proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
ferama committed Oct 15, 2024
1 parent 156d53f commit 16572fc
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 2 deletions.
9 changes: 8 additions & 1 deletion cmd/configs/config_template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ socksproxy:
listen_address: :1080
# OPTIONAL: if defined use a dedicated sshclient for the socksproxy
# sshclient:


# if set, enable a dns proxy over ssh connection
# the remote dns must accept tcp connections
dnsproxy:
listen_address: :53
remote_dns_address: 8.8.8.8:53
# OPTIONAL: if defined use a dedicated sshclient for the socksproxy
# sshclient:

# List of tunnels configuration. Requires that the sshclient section
# is configured too. We are going to use one ssh connection
Expand Down
45 changes: 45 additions & 0 deletions cmd/dns_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cmd

import (
"log"

"github.com/ferama/rospo/cmd/cmnflags"
"github.com/ferama/rospo/pkg/sshc"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(dnsProxyCmd)
// sshc options
cmnflags.AddSshClientFlags(dnsProxyCmd.Flags())

dnsProxyCmd.Flags().StringP("listen-address", "l", ":53", "the dns proxy listener address")
dnsProxyCmd.Flags().StringP("remote-dns-server", "d", sshc.DEFAULT_DNS_SERVER, "the dns address to reach through sshc")
}

var dnsProxyCmd = &cobra.Command{
Use: "dns-proxy [user@]host[:port]",
Short: "Starts a dns proxy",
Long: `Starts a local dns server that sends its request through the ssh tunnel
to the configured DNS server.
`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
sshcConf := cmnflags.GetSshClientConf(cmd, args[0])
conn := sshc.NewSshConnection(sshcConf)
go conn.Start()

listenAddress, _ := cmd.Flags().GetString("listen-address")
remoteDnsServer, _ := cmd.Flags().GetString("remote-dns-server")

conf := &sshc.DnsProxyConf{
ListenAddress: listenAddress,
RemoteDnsAddress: &remoteDnsServer,
}
proxy := sshc.NewDnsProxy(conn, conf)
err := proxy.Start()
if err != nil {
log.Fatalln(err)
}
},
}
23 changes: 22 additions & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var runCmd = &cobra.Command{
somethingRun = true
}

if conf.Tunnel != nil && len(conf.Tunnel) > 0 {
if len(conf.Tunnel) > 0 {
for _, c := range conf.Tunnel {
if c.SshClientConf != nil {
conn := sshc.NewSshConnection(c.SshClientConf)
Expand Down Expand Up @@ -87,6 +87,27 @@ var runCmd = &cobra.Command{
}()
}

if conf.DnsProxy != nil {
var dnsProxy *sshc.DnsProxy
if conf.DnsProxy.SshClientConf == nil {
failIfNoClient("dns proxy")
dnsProxy = sshc.NewDnsProxy(sshConn, conf.DnsProxy)
} else {
proxySshConn := sshc.NewSshConnection(conf.SocksProxy.SshClientConf)
go proxySshConn.Start()
dnsProxy = sshc.NewDnsProxy(proxySshConn, conf.DnsProxy)
}
somethingRun = true

go func() {
err := dnsProxy.Start()
if err != nil {
log.Fatal(err)
}
}()

}

if somethingRun {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
Expand Down
File renamed without changes.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/stretchr/testify v1.8.3 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.22.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
Expand Down Expand Up @@ -63,6 +65,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
Expand All @@ -71,6 +75,8 @@ golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -94,6 +100,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
2 changes: 2 additions & 0 deletions pkg/conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Config struct {
Tunnel []*tun.TunnelConf `yaml:"tunnel"`
SshD *sshd.SshDConf `yaml:"sshd"`
SocksProxy *sshc.SocksProxyConf `yaml:"socksproxy"`
DnsProxy *sshc.DnsProxyConf `yaml:"dnsproxy"`
}

// LoadConfig parses the [config].yaml file and loads its values
Expand All @@ -31,6 +32,7 @@ func LoadConfig(filePath string) (*Config, error) {
nil,
nil,
nil,
nil,
}

decoder := yaml.NewDecoder(f)
Expand Down
7 changes: 7 additions & 0 deletions pkg/sshc/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ type SocksProxyConf struct {
SshClientConf *SshClientConf `yaml:"sshclient"`
}

type DnsProxyConf struct {
ListenAddress string `yaml:"listen_address"`
RemoteDnsAddress *string `yaml:"remote_dns_address"`
// use a dedicated ssh client. if nil use the global one
SshClientConf *SshClientConf `yaml:"sshclient"`
}

// GetServerEndpoint Builds a server endpoint object from the Server string
func (c *SshClientConf) GetServerEndpoint() *utils.Endpoint {
return utils.NewEndpoint(c.ServerURI)
Expand Down
138 changes: 138 additions & 0 deletions pkg/sshc/dns_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package sshc

import (
"encoding/binary"
"fmt"
"net"

"github.com/miekg/dns"
)

const DEFAULT_DNS_SERVER = "1.1.1.1:53"

type DnsProxy struct {
sshConn *SshConnection
remoteDnsServer string
proxyListenAddr string
}

func NewDnsProxy(sshConn *SshConnection, conf *DnsProxyConf) *DnsProxy {
remoteDnsServer := DEFAULT_DNS_SERVER
if conf.RemoteDnsAddress != nil {
remoteDnsServer = *conf.RemoteDnsAddress
}
p := &DnsProxy{
sshConn: sshConn,
proxyListenAddr: conf.ListenAddress,
remoteDnsServer: remoteDnsServer,
}

return p
}

// resolveDomain sends a DNS query for a domain name over TCP
func (p *DnsProxy) resolveDomain(conn net.Conn, msg *dns.Msg) ([]byte, error) {
// Pack the DNS message (with the original transaction ID)
query, err := msg.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack DNS message: %v", err)
}

queryLength := make([]byte, 2)
binary.BigEndian.PutUint16(queryLength, uint16(len(query)))

if _, err := conn.Write(append(queryLength, query...)); err != nil {
return nil, fmt.Errorf("failed to send DNS query: %v", err)
}

responseLengthBytes := make([]byte, 2)
if _, err := conn.Read(responseLengthBytes); err != nil {
return nil, fmt.Errorf("failed to read response length: %v", err)
}
responseLength := binary.BigEndian.Uint16(responseLengthBytes)

response := make([]byte, responseLength)
if _, err := conn.Read(response); err != nil {
return nil, fmt.Errorf("failed to read DNS response: %v", err)
}

return response, nil
}

func (p *DnsProxy) handleDNSQuery(udpConn *net.UDPConn, clientAddr *net.UDPAddr, query []byte) {
// Unpack the DNS message
msg := new(dns.Msg)
if err := msg.Unpack(query); err != nil {
log.Printf("failed to unpack DNS query: %v", err)
return
}

// Extract the domain name from the query
if len(msg.Question) == 0 {
log.Printf("invalid DNS query: no question section")
return
}

originalID := msg.Id // Preserve the original transaction ID

conn, err := p.sshConn.Client.Dial("tcp", p.remoteDnsServer)
if err != nil {
log.Printf("unable to connect to remote dns server: %v", err)
return
}
// Resolve the domain through the proxy
dnsResponse, err := p.resolveDomain(conn, msg)
if err != nil {
log.Printf("failed to resolve domain: %v", err)
return
}

// Unpack the DNS response
reply := new(dns.Msg)
if err := reply.Unpack(dnsResponse); err != nil {
log.Printf("failed to unpack DNS response: %v", err)
return
}

// Set the original transaction ID back into the response
reply.Id = originalID

// Pack the modified response (with correct ID)
finalResponse, err := reply.Pack()
if err != nil {
log.Printf("failed to pack final DNS response: %v", err)
return
}

// Send the DNS response back to the client
if _, err := udpConn.WriteToUDP(finalResponse, clientAddr); err != nil {
log.Printf("failed to send DNS response: %v", err)
return
}
}

func (p *DnsProxy) Start() error {
p.sshConn.ReadyWait()

addr, err := net.ResolveUDPAddr("udp", p.proxyListenAddr)
if err != nil {
return fmt.Errorf("failed to resolve UDP address: %v", err)
}

udpConn, err := net.ListenUDP("udp", addr)
if err != nil {
return fmt.Errorf("failed to listen on UDP port 53: %v", err)
}
defer udpConn.Close()
log.Printf("dns-proxy listening on UDP: %s. Using remote dns: %s", p.proxyListenAddr, p.remoteDnsServer)

// Handle incoming DNS queries
buf := make([]byte, 4096)
for {
n, clientAddr, err := udpConn.ReadFromUDP(buf)
if err != nil {
continue
}
go p.handleDNSQuery(udpConn, clientAddr, buf[:n])
}
}

0 comments on commit 16572fc

Please sign in to comment.