From aa6db28d85ea2e425fa722b84d2af3232f993344 Mon Sep 17 00:00:00 2001 From: Dan Vittegleo Date: Tue, 7 Nov 2017 00:43:20 -0800 Subject: [PATCH] * Add IPv6 support * Fix: Create SSH key if one doesn't exist in account * Fix: Retry firewall creation * Add iptables configuration with ICMP/SSH rate limiting * Migrate to ECDSA keys instead of RSA --- README.md | 15 ++++--- cmd/rm.go | 4 +- deploy/deploy.go | 36 +++++++++++++--- deploy/rm.go | 14 +++--- doclient/do.go | 27 ++++++++++-- genconfig/android_template.go | 5 +-- genconfig/apple_template.go | 27 ++++++------ services/coreos/coreos.go | 81 +++++++++++++++++++++++++++++++++-- services/dosxvpn/dosxvpn.go | 23 +++++++++- services/pihole/pihole.go | 10 +++++ sshclient/ssh.go | 4 ++ web/handler.go | 2 +- 12 files changed, 204 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 9c4b34e..2480315 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,17 @@ ``` ### CLI Examples -* Deploy a new VPN and configure for immediate use +* Deploy a new VPN droplet and configure OSX VPN ```sh ./dosxvpn deploy --region sfo2 --auto-configure ``` -* List dosxvpn VPN instances +* List dosxvpn VPN droplets ```sh ./dosxvpn ls ``` -* Remove dosxvpn VPN instance +* Remove dosxvpn VPN droplet and OSX VPN profile ```sh - ./dosxvpn rm --name + ./dosxvpn rm --name --remove-profile ``` ## FAQ @@ -54,9 +54,10 @@ 4. How much does this cost? This launches a 512MB DigitalOcean droplet that costs $5/month currently. 5. What is the bandwidth limit? The 512MB DigitalOcean droplet has a 1TB bandwidth limit. This does not appear to be strictly enforced. 6. Where does dosxvpn store VPN configuration files? You can find all deployed VPN configuration files in your ~/.dosxvpn directory. -7. Are you going to support other VPS providers? Not right now. -8. Will this make me completely anonymous? No, absolutely not. All of your traffic is going through a VPS which could be traced back to your account. You can also be tracked still with [browser fingerprinting](https://panopticlick.eff.org/), etc. Your [IP address may still leak](https://ipleak.net/) due to WebRTC, Flash, etc. -9. How do I uninstall this thing on OSX? You can uninstall through the Web interface, which will also remove the running droplet in your DigitalOcean account. Alternatively go to System Preferences->Network, click on dosxvpn-* and click the '-' button in the bottom left to delete the VPN. Don't forget to also remove the droplet that is deployed in your DigitalOcean account. +7. How do I SSH into the deployed droplet? Assuming you had public SSH keys uploaded to your DigitalOcean account when the VPN was deployed, all of those keys should be authorized for access. You can SSH using any of those keys: `ssh -i core@`. If you had no SSH keys uploaded to your DigitalOcean account, then a temporary key was autogenerated for you and you will need to redeploy if you want SSH access. +8. Are you going to support other VPS providers? Not right now. +9. Will this make me completely anonymous? No, absolutely not. All of your traffic is going through a VPS which could be traced back to your account. You can also be tracked still with [browser fingerprinting](https://panopticlick.eff.org/), etc. Your [IP address may still leak](https://ipleak.net/) due to WebRTC, Flash, etc. +10. How do I uninstall this thing on OSX? You can uninstall through the Web interface, which will also remove the running droplet in your DigitalOcean account. Alternatively go to System Preferences->Network, click on dosxvpn-* and click the '-' button in the bottom left to delete the VPN. Don't forget to also remove the droplet that is deployed in your DigitalOcean account. # Powered By * [strongSwan](https://strongswan.org/) - IPsec-based VPN software diff --git a/cmd/rm.go b/cmd/rm.go index fb54662..4bc7a20 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -9,6 +9,7 @@ import ( ) var name string +var removeProfile bool var rmCmd = &cobra.Command{ Use: "rm", @@ -23,7 +24,7 @@ var rmCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - _, err := deploy.RemoveVPN(getCliToken(), name) + _, err := deploy.RemoveVPN(getCliToken(), name, removeProfile) if err != nil { log.Fatal(err) } @@ -34,4 +35,5 @@ var rmCmd = &cobra.Command{ func init() { RootCmd.AddCommand(rmCmd) rmCmd.Flags().StringVar(&name, "name", "", "Name of droplet to remove") + rmCmd.Flags().BoolVar(&removeProfile, "remove-profile", false, "Remove VPN profile as well (only for OSX).") } diff --git a/deploy/deploy.go b/deploy/deploy.go index c7954d3..9245373 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -26,9 +26,10 @@ import ( ) const ( - DropletBaseName = "dosxvpn" - DropletImage = "coreos-beta" - DropletSize = "512mb" + DropletBaseName = "dosxvpn" + DropletImage = "coreos-beta" + DropletSize = "512mb" + AutogeneratedSSHKey = "dosxvpn" ) var ( @@ -91,7 +92,21 @@ func (d *Deployment) Run() error { log.Println("Getting initial IP...") initialPublicIP, _ := getPublicIp() d.InitialPublicIP = initialPublicIP - log.Println("Initial IP is", d.InitialPublicIP) + log.Println("Initial IP is:", d.InitialPublicIP) + + log.Println("Getting account SSH keys...") + accountSSHKeys, err := d.doClient.GetAccountSSHKeys() + if err != nil { + log.Fatal(err) + } + if len(accountSSHKeys) == 0 { + log.Println("Did not find an SSH key. Generating an SSH key for account...") + _, err := d.doClient.CreateSSHKey(AutogeneratedSSHKey, d.sshClient.GetPublicKey()) + if err != nil { + log.Fatal(err) + } + } + log.Println("Finished getting account SSH keys...") log.Println("Creating droplet...") dropletID, err := d.doClient.CreateDroplet(d.Name, d.Region, DropletSize, d.userData, DropletImage) @@ -111,9 +126,16 @@ func (d *Deployment) Run() error { d.VPNIPAddress = d.dropletIP log.Println("Creating firewall...") - err = d.doClient.CreateFirewall(d.Name, d.dropletID) - if err != nil { - log.Fatal(err) + for attempt := 0; attempt < 5; attempt++ { + time.Sleep(time.Duration(attempt) * time.Second) + + err = d.doClient.CreateFirewall(d.Name, d.dropletID) + if err != nil { + continue + } + if attempt >= 5 { + log.Fatalf("Timeout waiting to create firewall for droplet %v", dropletID) + } } log.Println("Finished creating firewall...") diff --git a/deploy/rm.go b/deploy/rm.go index 3285b65..47a1a5c 100644 --- a/deploy/rm.go +++ b/deploy/rm.go @@ -8,7 +8,7 @@ import ( "github.com/dan-v/dosxvpn/vpn" ) -func RemoveVPN(token, name string) ([]string, error) { +func RemoveVPN(token, name string, removeProfile bool) ([]string, error) { log.Printf("Listing droplets..") client := doclient.New(token) droplets, err := client.ListDroplets() @@ -41,12 +41,14 @@ func RemoveVPN(token, name string) ([]string, error) { } } - log.Printf("Removing OSX VPN profile for %s", name) - err = vpn.OSXRemoveVPN(name) - if err != nil { - log.Printf("Failed to remove OSX VPN profile for %s. %v", name, err) + if removeProfile { + log.Printf("Removing OSX VPN profile for %s", name) + err = vpn.OSXRemoveVPN(name) + if err != nil { + log.Printf("Failed to remove OSX VPN profile for %s. %v", name, err) + } + log.Printf("Finished removing OSX VPN profile for %s", name) } - log.Printf("Finished removing OSX VPN profile for %s", name) return removedDroplets, nil } diff --git a/doclient/do.go b/doclient/do.go index 1638a8e..19c501d 100644 --- a/doclient/do.go +++ b/doclient/do.go @@ -56,6 +56,18 @@ func (c *Client) WaitForDropletIP(dropletID int) (ip string, err error) { return ip, nil } +func (c *Client) CreateSSHKey(name, publicKey string) (id int, err error) { + createRequest := &godo.KeyCreateRequest{ + Name: "dosxvpn", + PublicKey: publicKey, + } + key, _, err := c.doClient.Keys.Create(context.TODO(), createRequest) + if err != nil { + return 0, err + } + return key.ID, nil +} + func (c *Client) CreateDroplet(name, region, size, userData, image string) (id int, err error) { createRequest := &godo.DropletCreateRequest{ Name: name, @@ -65,9 +77,13 @@ func (c *Client) CreateDroplet(name, region, size, userData, image string) (id i Image: godo.DropletCreateImage{ Slug: image, }, + IPv6: true, } - accountSSHKeys, err := c.getAccountSSHKeys() + accountSSHKeys, err := c.GetAccountSSHKeys() + if err != nil { + return 0, err + } for _, key := range accountSSHKeys { keyToAdd := godo.DropletCreateSSHKey{ID: key.ID} createRequest.SSHKeys = append(createRequest.SSHKeys, keyToAdd) @@ -138,8 +154,7 @@ func (c *Client) DeleteFirewall(firewallID string) error { return nil } -func (c *Client) getAccountSSHKeys() ([]godo.Key, error) { - // Query all the SSH keys on the account so we can include them in the droplet. +func (c *Client) GetAccountSSHKeys() ([]godo.Key, error) { keys, _, err := c.doClient.Keys.List(context.TODO(), nil) if err != nil { return nil, err @@ -149,6 +164,12 @@ func (c *Client) getAccountSSHKeys() ([]godo.Key, error) { func (c *Client) generateInboundFirewallRules() []godo.InboundRule { return []godo.InboundRule{ + { + Protocol: "icmp", + Sources: &godo.Sources{ + Addresses: []string{"0.0.0.0/0", "::/0"}, + }, + }, { Protocol: "tcp", PortRange: "22", diff --git a/genconfig/android_template.go b/genconfig/android_template.go index 1ec8814..01968ca 100644 --- a/genconfig/android_template.go +++ b/genconfig/android_template.go @@ -1,7 +1,6 @@ package genconfig -const androidConfigTemplate = ` -{ +const androidConfigTemplate = `{ "uuid": "{{.UUID}}", "name": "{{.Name}}", "type": "ikev2-cert", @@ -13,7 +12,7 @@ const androidConfigTemplate = ` "block-ipv6": true }, "local": { - "id": "client@{{.IP}}", + "id": "{{.IP}}", "p12": "{{.PrivateKey}}" }, "mtu": 1280 diff --git a/genconfig/apple_template.go b/genconfig/apple_template.go index 62c2e48..60e18be 100644 --- a/genconfig/apple_template.go +++ b/genconfig/apple_template.go @@ -1,7 +1,6 @@ package genconfig -const mobileConfigTemplate = ` - +const mobileConfigTemplate = ` @@ -65,14 +64,14 @@ const mobileConfigTemplate = ` Certificate ChildSecurityAssociationParameters - DiffieHellmanGroup - 2 + DiffieHellmanGroup + 19 EncryptionAlgorithm - 3DES + AES-128-GCM IntegrityAlgorithm - SHA1-96 + SHA2-512 LifeTimeInMinutes - 1440 + 20 DeadPeerDetectionRate Medium @@ -87,18 +86,22 @@ const mobileConfigTemplate = ` IKESecurityAssociationParameters DiffieHellmanGroup - 2 + 19 EncryptionAlgorithm - 3DES + AES-128-GCM IntegrityAlgorithm - SHA1-96 + SHA2-512 LifeTimeInMinutes - 1440 + 20 LocalIdentifier - client@{{.IP}} + {{.IP}} PayloadCertificateUUID {{.UUID1}} + CertificateType + ECDSA256 + ServerCertificateIssuerCommonName + {{.IP}} RemoteAddress {{.IP}} RemoteIdentifier diff --git a/services/coreos/coreos.go b/services/coreos/coreos.go index 2ee216b..fa80396 100644 --- a/services/coreos/coreos.go +++ b/services/coreos/coreos.go @@ -18,6 +18,75 @@ write_files: AllowUsers core PasswordAuthentication no ChallengeResponseAuthentication no + - path: /var/lib/iptables/rules-save + permissions: 0644 + owner: root:root + content: | + *nat + :PREROUTING ACCEPT [0:0] + :POSTROUTING ACCEPT [0:0] + -A POSTROUTING -s 192.168.99.0/24 -m policy --pol none --dir out -j MASQUERADE + COMMIT + *filter + :INPUT DROP [0:0] + :FORWARD DROP [0:0] + :OUTPUT ACCEPT [0:0] + -A INPUT -i lo -j ACCEPT + -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + -A INPUT -p esp -j ACCEPT + -A INPUT -p ah -j ACCEPT + -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT + -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT + -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH + -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 --rttl --name SSH -j DROP + -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT + -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT + -A INPUT -d 1.1.1.1 -p udp -j ACCEPT + -A INPUT -d 1.1.1.1 -p tcp -j ACCEPT + -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + -A FORWARD -m conntrack --ctstate NEW -s 192.168.99.0/24 -m policy --pol ipsec --dir in -j ACCEPT + COMMIT + - path: /var/lib/ip6tables/rules-save + permissions: 0644 + owner: root:root + content: | + *nat + :PREROUTING ACCEPT [0:0] + :POSTROUTING ACCEPT [0:0] + -A POSTROUTING -s fd9d:bc11:4020::/48 -m policy --pol none --dir out -j MASQUERADE + COMMIT + *filter + :INPUT DROP [0:0] + :FORWARD DROP [0:0] + :OUTPUT ACCEPT [0:0] + :ICMPV6-CHECK - [0:0] + :ICMPV6-CHECK-LOG - [0:0] + -A INPUT -i lo -j ACCEPT + -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + -A INPUT -p esp -j ACCEPT + -A INPUT -m ah -j ACCEPT + -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT + -A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT + -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT + -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT + -A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT + -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH + -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 --rttl --name SSH -j DROP + -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT + -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT + -A INPUT -d fd9d:bc11:4020::/48 -p udp -j ACCEPT + -A INPUT -d fd9d:bc11:4020::/48 -p tcp -j ACCEPT + -A FORWARD -j ICMPV6-CHECK + -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + -A FORWARD -m conntrack --ctstate NEW -s fd9d:bc11:4020::/48 -m policy --pol ipsec --dir in -j ACCEPT + -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-solicitation -j ICMPV6-CHECK-LOG + -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-advertisement -j ICMPV6-CHECK-LOG + -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-solicitation -j ICMPV6-CHECK-LOG + -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-advertisement -j ICMPV6-CHECK-LOG + -A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP " + -A ICMPV6-CHECK-LOG -j DROP + COMMIT + coreos: update: reboot-strategy: reboot @@ -25,8 +94,14 @@ coreos: window-start: 10:00 window-length: 1h units: - - name: "etcd2.service" - command: "start" + - name: etcd2.service + command: start + - name: iptables-restore.service + enable: true + command: start + - name: ip6tables-restore.service + enable: true + command: start - name: dummy-interface.service command: start content: | @@ -36,6 +111,6 @@ coreos: [Service] User=root Type=oneshot - ExecStart=/bin/sh -c "modprobe dummy; ip link set dummy0 up; ifconfig dummy0 1.1.1.1/32; echo 1.1.1.1 pi.hole >> /etc/hosts" + ExecStart=/bin/sh -c "modprobe dummy; ip link set dummy0 up; ifconfig dummy0 1.1.1.1/32" ` } diff --git a/services/dosxvpn/dosxvpn.go b/services/dosxvpn/dosxvpn.go index b44006c..bc58f84 100644 --- a/services/dosxvpn/dosxvpn.go +++ b/services/dosxvpn/dosxvpn.go @@ -4,6 +4,27 @@ type Service struct{} func (s Service) UserData() string { return ` + - name: dosxvpn-sysctl.service + enable: true + content: | + [Unit] + Description=Handles settings for sysctl + + [Service] + Type=oneshot + User=root + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.ip_forward=1 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.accept_source_route=0 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.accept_source_route=0 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.accept_redirects=0 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.accept_redirects=0 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.secure_redirects=0 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.secure_redirects=0 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.rp_filter=1 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.rp_filter=1 + ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 + ExecStart=-/usr/bin/echo echo 1 > /proc/sys/net/ipv4/route/flush - name: dosxvpn-update.service content: | [Unit] @@ -37,7 +58,7 @@ func (s Service) UserData() string { ExecStartPre=-/usr/bin/docker kill dosxvpn ExecStartPre=-/usr/bin/docker rm dosxvpn ExecStartPre=/usr/bin/docker pull dosxvpn/strongswan - ExecStart=/usr/bin/docker run --name dosxvpn --privileged --net=host -v ipsec.d:/etc/ipsec.d -v strongswan.d:/etc/strongswan.d -v /lib/modules:/lib/modules -v /etc/localtime:/etc/localtime -e VPN_DNS=1.1.1.1 -e VPN_DOMAIN=$public_ipv4 dosxvpn/strongswan + ExecStart=/usr/bin/docker run --name dosxvpn --privileged --net=host -v ipsec.d:/etc/ipsec.d -v strongswan.d:/etc/strongswan.d -v /lib/modules:/lib/modules -v /etc/localtime:/etc/localtime -e VPN_DOMAIN=$public_ipv4 dosxvpn/strongswan ExecStop=/usr/bin/docker stop dosxvpn ` } diff --git a/services/pihole/pihole.go b/services/pihole/pihole.go index 36b3f9f..3dc8c5f 100644 --- a/services/pihole/pihole.go +++ b/services/pihole/pihole.go @@ -4,6 +4,16 @@ type Service struct{} func (s Service) UserData() string { return ` + - name: pihole-etc-host.service + command: start + content: | + [Unit] + Description=pihole /etc/hosts entry + + [Service] + User=root + Type=oneshot + ExecStart=/bin/sh -c "echo 1.1.1.1 pi.hole >> /etc/hosts" - name: pihole.service command: start content: | diff --git a/sshclient/ssh.go b/sshclient/ssh.go index 99ef74d..05bce48 100644 --- a/sshclient/ssh.go +++ b/sshclient/ssh.go @@ -34,6 +34,10 @@ func New() (*Client, error) { return ssh, nil } +func (s *Client) GetPublicKey() string { + return string(ssh.MarshalAuthorizedKey(s.KeyPair.PublicKey)) +} + func (s *Client) openSession(user, host string) (*ssh.Session, error) { signer, err := ssh.NewSignerFromKey(s.KeyPair.PrivateKey) if err != nil { diff --git a/web/handler.go b/web/handler.go index db75639..803bff6 100644 --- a/web/handler.go +++ b/web/handler.go @@ -132,7 +132,7 @@ func (h *handler) delete(rw http.ResponseWriter, req *http.Request) { rw.Write([]byte("Need to specify droplet")) return } - _, err := deploy.RemoveVPN(h.token, droplet) + _, err := deploy.RemoveVPN(h.token, droplet, true) if err != nil { rw.Write([]byte(fmt.Sprintf("Failed to remove VPN: %v", err))) return