diff --git a/cmd/configs/config_template.yaml b/cmd/configs/config_template.yaml index 215c93d..2fae734 100644 --- a/cmd/configs/config_template.yaml +++ b/cmd/configs/config_template.yaml @@ -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 diff --git a/cmd/dns_proxy.go b/cmd/dns_proxy.go new file mode 100644 index 0000000..0ac7e5b --- /dev/null +++ b/cmd/dns_proxy.go @@ -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) + } + }, +} diff --git a/cmd/run.go b/cmd/run.go index 58a7c11..2034c86 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -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) @@ -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) diff --git a/cmd/proxy.go b/cmd/socks_proxy.go similarity index 100% rename from cmd/proxy.go rename to cmd/socks_proxy.go diff --git a/go.mod b/go.mod index 979ab3c..9bf71b3 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 273cd37..70c0ba3 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/pkg/conf/config.go b/pkg/conf/config.go index 4c61a69..5f8cb8c 100644 --- a/pkg/conf/config.go +++ b/pkg/conf/config.go @@ -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 @@ -31,6 +32,7 @@ func LoadConfig(filePath string) (*Config, error) { nil, nil, nil, + nil, } decoder := yaml.NewDecoder(f) diff --git a/pkg/sshc/conf.go b/pkg/sshc/conf.go index f86f6b6..c5028a2 100644 --- a/pkg/sshc/conf.go +++ b/pkg/sshc/conf.go @@ -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) diff --git a/pkg/sshc/dns_proxy.go b/pkg/sshc/dns_proxy.go new file mode 100644 index 0000000..401ef9a --- /dev/null +++ b/pkg/sshc/dns_proxy.go @@ -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]) + } +}