From a0f8afb97d2218b4f987779c5b2bb247ead1c66e Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Fri, 11 Mar 2022 13:50:17 -0500 Subject: Remove dependency on statik and use go:embed This removes a dependency by using the built-in go:embed functionality introduce in Go 1.16 instead of statik for embedding files. This means that Go 1.16+ would now be required to build the VPN. Signed-off-by: Sam Whited --- go.mod | 50 +- go.sum | 16 +- helpers/bitmask-root | 1038 ------------------------ helpers/se.leap.bitmask.policy | 23 - helpers/se.leap.bitmask.snap.policy | 23 - pkg/pickle/helpers.go | 28 +- pkg/pickle/helpers/bitmask-root | 1038 ++++++++++++++++++++++++ pkg/pickle/helpers/se.leap.bitmask.policy | 23 + pkg/pickle/helpers/se.leap.bitmask.snap.policy | 23 + pkg/pickle/statik/statik.go | 14 - 10 files changed, 1139 insertions(+), 1137 deletions(-) delete mode 100644 helpers/bitmask-root delete mode 100644 helpers/se.leap.bitmask.policy delete mode 100644 helpers/se.leap.bitmask.snap.policy create mode 100644 pkg/pickle/helpers/bitmask-root create mode 100644 pkg/pickle/helpers/se.leap.bitmask.policy create mode 100644 pkg/pickle/helpers/se.leap.bitmask.snap.policy delete mode 100644 pkg/pickle/statik/statik.go diff --git a/go.mod b/go.mod index 5546621..662d700 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,60 @@ module 0xacab.org/leap/bitmask-vpn -go 1.14 +go 1.17 require ( 0xacab.org/leap/shapeshifter v0.0.0-20191029173606-85d3e8ac43e2 git.torproject.org/pluggable-transports/goptlib.git v1.1.0 git.torproject.org/pluggable-transports/snowflake.git v1.1.0 - github.com/OperatorFoundation/obfs4 v0.0.0-20161108041644-17f2cb99c264 // indirect - github.com/OperatorFoundation/shapeshifter-ipc v0.0.0-20170814234159-11746ba927e0 // indirect - github.com/OperatorFoundation/shapeshifter-transports v0.0.0-20191101030951-7a751b0500f4 // indirect github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a - github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/apparentlymart/go-openvpn-mgmt v0.0.0-20161009010951-9a305aecd7f2 github.com/cretz/bine v0.2.0 - github.com/dchest/siphash v1.2.1 // indirect - github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19 github.com/pion/webrtc/v3 v3.0.15 - github.com/rakyll/statik v0.1.7 github.com/sevlyar/go-daemon v0.1.5 github.com/smartystreets/goconvey v1.6.4 github.com/xtaci/kcp-go/v5 v5.6.1 github.com/xtaci/smux v1.5.15 golang.org/x/sys v0.0.0-20210423082822-04245dca01da ) + +require ( + github.com/OperatorFoundation/obfs4 v0.0.0-20161108041644-17f2cb99c264 // indirect + github.com/OperatorFoundation/shapeshifter-ipc v0.0.0-20170814234159-11746ba927e0 // indirect + github.com/OperatorFoundation/shapeshifter-transports v0.0.0-20191101030951-7a751b0500f4 // indirect + github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect + github.com/dchest/siphash v1.2.1 // indirect + github.com/google/uuid v1.2.0 // indirect + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect + github.com/klauspost/cpuid v1.3.1 // indirect + github.com/klauspost/reedsolomon v1.9.9 // indirect + github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 // indirect + github.com/pion/datachannel v1.4.21 // indirect + github.com/pion/dtls/v2 v2.0.8 // indirect + github.com/pion/ice/v2 v2.0.15 // indirect + github.com/pion/interceptor v0.0.10 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns v0.0.4 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.6 // indirect + github.com/pion/rtp v1.6.2 // indirect + github.com/pion/sctp v1.7.11 // indirect + github.com/pion/sdp/v3 v3.0.4 // indirect + github.com/pion/srtp/v2 v2.0.2 // indirect + github.com/pion/stun v0.3.5 // indirect + github.com/pion/transport v0.12.3 // indirect + github.com/pion/turn/v2 v2.0.5 // indirect + github.com/pion/udp v0.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect + github.com/templexxx/cpu v0.0.7 // indirect + github.com/templexxx/xorsimd v0.4.1 // indirect + github.com/tjfoc/gmsm v1.3.2 // indirect + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect + golang.org/x/mod v0.3.0 // indirect + golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect + golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect +) diff --git a/go.sum b/go.sum index 07b1a13..917f762 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= @@ -309,8 +309,6 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= -github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -339,11 +337,11 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/cpu v0.0.7 h1:pUEZn8JBy/w5yzdYWgx+0m0xL9uk6j4K91C5kOViAzo= @@ -358,6 +356,7 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xtaci/kcp-go/v5 v5.6.1 h1:Pwn0aoeNSPF9dTS7IgiPXn0HEtaIlVb6y5UKWPsx8bI= github.com/xtaci/kcp-go/v5 v5.6.1/go.mod h1:W3kVPyNYwZ06p79dNwFWQOVFrdcBpDBsdyvK8moQrYo= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= github.com/xtaci/smux v1.5.15 h1:6hMiXswcleXj5oNfcJc+DXS8Vj36XX2LaX98udog6Kc= github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= @@ -382,14 +381,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191105034135-c7e5f84aec59 h1:PyXRxSVbvzDGuqYXjHndV7xDzJ7w2K8KD9Ef8GB7KOE= -golang.org/x/crypto v0.0.0-20191105034135-c7e5f84aec59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= @@ -421,7 +417,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -431,7 +426,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -465,8 +459,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -477,7 +469,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M= golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -552,6 +543,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helpers/bitmask-root b/helpers/bitmask-root deleted file mode 100644 index d33091c..0000000 --- a/helpers/bitmask-root +++ /dev/null @@ -1,1038 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2014-2019 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -""" -This is a privileged helper script for safely running certain commands as root. -It should only be called by the Bitmask application. - -Expected paths: - - When installed by distro path: - /usr/sbin/bitmask-root - - When installed by bundle or from git: - /usr/local/sbin/bitmask-root - - When installed by snap: - /snap/bin/riseup-vpn.bitmask-root - -USAGE: - bitmask-root firewall stop - bitmask-root firewall start [restart] GATEWAY1 GATEWAY2 ... - bitmask-root openvpn stop - bitmask-root openvpn start CONFIG1 CONFIG1 ... - -All actions return exit code 0 for success, non-zero otherwise. - -The `openvpn start` action is special: it calls exec on openvpn and replaces -the current process. If the `restart` parameter is passed, the firewall will -not be teared down in the case of an error during launch. -""" -import ipaddress -import os -import re -import signal -import socket -import syslog -import subprocess -import sys -import stat -import traceback - -cmdcheck = subprocess.check_output - -# -# CONSTANTS - -def get_no_group_name(): - """ - Return the right group name to use for the current OS. - Examples: - - Ubuntu: nogroup - - Arch: nobody - - :rtype: str or None - """ - import grp - try: - grp.getgrnam('nobody') - return 'nobody' - except KeyError: - try: - grp.getgrnam('nogroup') - return 'nogroup' - except KeyError: - return None - -def is_ipv6_disabled(): - """ - Return True if ipv6 support is disabled by the kernel. - """ - code = os.system("sysctl -a 2>/dev/null | grep all.disable_ipv6 | grep 1") - return code == 0 - -def tostr(s): - return s.decode('utf-8') - -VERSION = "16" -SCRIPT = "bitmask-root" -NAMESERVER_TCP = "10.41.0.1" -NAMESERVER_UDP = "10.42.0.1" - -if os.getenv("UDP") == "1": - NAMESERVER = NAMESERVER_UDP -else: - NAMESERVER = NAMESERVER_TCP -BITMASK_CHAIN = "bitmask" -BITMASK_CHAIN_NAT_OUT = "bitmask" -BITMASK_CHAIN_NAT_POST = "bitmask_postrouting" -LOCAL_INTERFACE = "lo" - -def swhich(binary): - """ - Find the path to binary in sbin - - :rtype: str - """ - for folder in ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/local/sbin"]: - path = os.path.join(folder, binary) - if os.path.isfile(path): - return path - - raise Exception("Can't find %s" % (binary,)) - -IP = swhich("ip") -IPTABLES = swhich("iptables") -IP6TABLES = swhich("ip6tables") -SYSCTL = swhich("sysctl") - -OPENVPN_USER = "nobody" -OPENVPN_GROUP = get_no_group_name() -LEAPOPENVPN = "LEAPOPENVPN" -OPENVPN_SYSTEM_BIN = "/usr/sbin/openvpn" # Debian location -OPENVPN_LEAP_BIN = "/usr/local/sbin/leap-openvpn" # installed by bundle -OPENVPN_SNAP_BIN = "/snap/bin/riseup-vpn.openvpn" # installed by snap - -FIXED_FLAGS = [ - "--setenv", "LEAPOPENVPN", "1", - "--nobind", - "--client", - "--dev", "tun", - "--tls-client", - "--remote-cert-tls", "server", - "--management-signal", - "--script-security", "1", - "--user", "nobody", - "--persist-key", - "--persist-local-ip", - "--tls-version-min", "1.0", -] - -if OPENVPN_GROUP is not None: - FIXED_FLAGS.extend(["--group", OPENVPN_GROUP]) - -if is_ipv6_disabled(): - FIXED_FLAGS.extend([ - "--pull-filter", "ignore", "ifconfig-ipv6", - "--pull-filter", "ignore", "route-ipv6"]) - -ALLOWED_FLAGS = { - "--remote": ["IP", "NUMBER", "PROTO"], - "--tls-cipher": ["CIPHER"], - "--cipher": ["CIPHER"], - "--auth": ["CIPHER"], - "--management": ["DIR||IP", "UNIXSOCKET||NUMBER"], - "--management-client-user": ["USER"], - "--route": ["IP", "IP", "NETGW"], - "--cert": ["FILE"], - "--key": ["FILE"], - "--ca": ["FILE"], - "--fragment": ["NUMBER"], - "--keepalive": ["NUMBER", "NUMBER"], - "--verb": ["NUMBER"], - "--management-client": [], - "--tun-ipv6": [], - "--log": ["LOGFILE"], -} - -PARAM_FORMATS = { - "NUMBER": lambda s: re.match("^\d+$", s), - "PROTO": lambda s: re.match("^(tcp|udp|tcp4|udp4)$", s), - "IP": lambda s: is_valid_address(s), - "CIPHER": lambda s: re.match("^[A-Z0-9-]+$", s), - "USER": lambda s: re.match( - "^[a-zA-Z0-9_\.\@][a-zA-Z0-9_\-\.\@]*\$?$", s), # IEEE Std 1003.1-2001 - "FILE": lambda s: os.path.isfile(s), - "DIR": lambda s: os.path.isdir(os.path.split(s)[0]), - "UNIXSOCKET": lambda s: s == "unix", - "NETGW": lambda s: s == "net_gateway", - "UID": lambda s: re.match("^[a-zA-Z0-9]+$", s), - "LOGFILE": lambda s: s == "/tmp/leap-vpn.log", -} - -# Determine Qubes OS version, if any -QUBES_PROXY = os.path.exists("/var/run/qubes/this-is-proxyvm") -if os.path.isdir("/etc/qubes"): - QUBES_CFG = "/rw/config/" - QUBES_IPHOOK = QUBES_CFG + "qubes-ip-change-hook" - QUBES_FW_SCRIPT = QUBES_CFG + "qubes-firewall-user-script" - if subprocess.call([IPTABLES, "--list", "QBS-FORWARD"]) == 0: - QUBES_VER = 4 - else: - QUBES_VER = 3 -else: - # not a Qubes system - QUBES_VER = 0 - - -DEBUG = os.getenv("DEBUG") -TEST = os.getenv("TEST") - -if DEBUG: - import logging - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s") - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) - ch.setFormatter(formatter) - logger = logging.getLogger(__name__) - logger.setLevel(logging.DEBUG) - logger.addHandler(ch) - -syslog.openlog(SCRIPT) - -# -# UTILITY -# - -def is_valid_address(value): - """ - Validate that the passed ip is a valid IP address. - - :param value: the value to be validated - :type value: str - :rtype: bool - """ - try: - socket.inet_aton(value) - return True - except Exception: - log("%s: ERROR: MALFORMED IP: %s!" % (SCRIPT, value)) - return False - - -def split_list(_list, regex): - """ - Split a list based on a regex: - e.g. split_list(["xx", "yy", "x1", "zz"], "^x") => [["xx", "yy"], ["x1", - "zz"]] - - :param _list: the list to be split. - :type _list: list - :param regex: the regex expression to filter with. - :type regex: str - - :rtype: list - """ - if not hasattr(regex, "match"): - regex = re.compile(regex) - result = [] - i = 0 - if not _list: - return result - while True: - if regex.match(_list[i]): - result.append([]) - while True: - result[-1].append(_list[i]) - i += 1 - if i >= len(_list) or regex.match(_list[i]): - break - else: - i += 1 - if i >= len(_list): - break - return result - - -def get_process_list(): - """ - Get a process list by reading `/proc` filesystem. - - :return: a list of tuples, each containing pid and command string. - :rtype: tuple if lists - """ - res = [] - pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] - - for pid in pids: - try: - res.append((pid, open( - os.path.join( - '/proc', pid, 'cmdline'), 'rb').read())) - except IOError: # proc has already terminated - continue - return filter(None, res) - - -def getIPv4AllowAddresses(): - lines = [] - try: - with open("/etc/bitmask/ipv4.allow", 'r') as f: - lines = [l.strip() for l in f.readlines()] - except FileNotFoundError: - return lines - - lines = filter(lambda x: ipaddress.ip_address(x).version == 4, lines) - return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) - -def getIPv6AllowAddresses(): - lines = [] - try: - with open("/etc/bitmask/ipv6.allow", 'r') as f: - lines = [l.strip() for l in f.readlines()] - except FileNotFoundError: - return lines - - lines = filter(lambda x: ipaddress.ip_address(x).version == 6, lines) - return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) - - -def run(command, *args, **options): - """ - Run an external command. - - Options: - - `check`: If True, check the command's output. bail if non-zero. (the - default is true unless detach or input is true) - `exitcode`: like `check`, but return exitcode instead of bailing. - `detach`: If True, run in detached process. - `input`: If True, open command for writing stream to, returning the Popen - object. - `throw`: If True, raise an exception if there is an error instead - of bailing. - """ - parts = [command] - parts.extend(args) - debug("%s run: %s " % (SCRIPT, " ".join(parts))) - - _check = options.get("check", True) - _detach = options.get("detach", False) - _input = options.get("input", False) - _exitcode = options.get("exitcode", False) - _throw = options.get("throw", False) - - if not (_check or _throw) or _detach or _input: - if _input: - return subprocess.Popen(parts, stdin=subprocess.PIPE) - else: - subprocess.Popen(parts) - return None - else: - try: - devnull = open('/dev/null', 'w') - subprocess.check_call(parts, stdout=devnull, stderr=devnull) - return 0 - except subprocess.CalledProcessError as exc: - if _exitcode: - if exc.returncode != 1: - # 0 or 1 is to be expected, but anything else - # should be logged. - debug("ERROR: Could not run %s: %s" % - (exc.cmd, exc.output), exception=exc) - return exc.returncode - elif _throw: - raise exc - else: - bail("ERROR: Could not run %s: %s" % (exc.cmd, exc.output), - exception=exc) - - -def log(msg=None, exception=None, priority=syslog.LOG_INFO): - """ - print and log warning message or exception. - - :param msg: optional error message. - :type msg: str - :param msg: optional exception. - :type msg: Exception - :param msg: syslog level - :type msg: one of LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, - LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG - """ - if msg is not None: - print("%s: %s" % (SCRIPT, msg)) - syslog.syslog(priority, msg) - if exception is not None: - if TEST or DEBUG: - traceback.print_exc() - syslog.syslog(priority, traceback.format_exc()) - - -def debug(msg=None, exception=None): - """ - Just like log, but is skipped unless DEBUG. Use syslog.LOG_INFO - even for debug messages (we don't want to miss them). - """ - if TEST or DEBUG: - log(msg, exception) - - -def bail(msg=None, exception=None): - """ - abnormal exit. like log(), but exits with error status code. - """ - log(msg, exception) - exit(1) - -# -# OPENVPN -# - - -def get_openvpn_bin(): - """ - Return the path for either the system openvpn or the one the - bundle has put there. - """ - if os.environ.get('SNAP') and os.path.isfile(OPENVPN_SNAP_BIN): - # the snap option should be removed from the debian package. - return OPENVPN_SNAP_BIN - - if os.path.isfile(OPENVPN_SYSTEM_BIN): - return OPENVPN_SYSTEM_BIN - - # the bundle option should also be removed from the debian. - if os.path.isfile(OPENVPN_LEAP_BIN): - return OPENVPN_LEAP_BIN - - -def parse_openvpn_flags(args): - """ - Take argument list from the command line and parse it, only allowing some - configuration flags. - - :type args: list - """ - result = [] - try: - for flag in split_list(args, "^--"): - flag_name = flag[0] - if flag_name in ALLOWED_FLAGS: - result.append(flag_name) - required_params = ALLOWED_FLAGS[flag_name] - if required_params: - # flatten if separated by spaces - flag_params = [i for sublist in map( - lambda s: s.split(), flag[1:]) for i in sublist] - if len(flag_params) != len(required_params): - log("%s: ERROR: not enough params for %s" % - (SCRIPT, flag_name)) - return None - for param, param_type in zip(flag_params, required_params): - for tpe in param_type.split("||"): - if PARAM_FORMATS[tpe](param): - result.append(param) - break - else: - log("%s: ERROR: Bad argument %s" % - (SCRIPT, param)) - return None - else: - log("WARNING: unrecognized openvpn flag %s" % flag_name) - return result - except Exception as exc: - log("%s: ERROR PARSING FLAGS: %s" % (SCRIPT, exc)) - if DEBUG: - logger.exception(exc) - return None - - -def openvpn_start(args): - """ - Launch openvpn, sanitizing input, and replacing the current process with - the openvpn process. - - :param args: arguments to be passed to openvpn - :type args: list - """ - openvpn_flags = parse_openvpn_flags(args) - if openvpn_flags: - OPENVPN = get_openvpn_bin() - flags = [OPENVPN] + FIXED_FLAGS + openvpn_flags - if DEBUG: - log("%s: running openvpn with flags:" % (SCRIPT,)) - log(flags) - # note: first argument to command is ignored, but customarily set to - # the command. - os.execv(OPENVPN, flags) - else: - bail('ERROR: could not parse openvpn options') - - -def openvpn_stop(args): - """ - Stop the openvpn that has likely been launched by bitmask. - - :param args: arguments to openvpn - :type args: list - """ - plist = get_process_list() - for pid, proc in plist: - if bytes("openvpn", 'utf-8') in proc and bytes(LEAPOPENVPN, 'utf-8') in proc: - os.kill(int(pid), signal.SIGTERM) - break - -# -# FIREWALL -# - - -def get_gateways(gateways): - """ - Filter a passed sequence of gateways, returning only the valid ones. - - :param gateways: a sequence of gateways to filter. - :type gateways: iterable - :rtype: iterable - """ - result = filter(is_valid_address, gateways) - if not result: - bail("ERROR: No valid gateways specified") - else: - return result - - -def get_default_device(): - """ - Retrieve the current default network device. - - :rtype: str - """ - routes = subprocess.check_output([IP, "route", "show"]) - match = re.search(rb"^default .*dev ([^\s]*) .*$", routes, flags=re.M) - if match and match.groups(): - return tostr(match.group(1)) - else: - bail("Could not find default device") - - -def get_local_network_ipv4(device): - """ - Get the local ipv4 addres for a given device. - - :param device: - :type device: str - """ - addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) - match = re.search(rb"^.*inet ([^ ]*) .*$", addresses, flags=re.M) - if match and match.groups(): - return tostr(match.group(1)) - else: - return None - - -def get_local_network_ipv6(device): - """ - Get the local ipv6 addres for a given device. - - :param device: - :type device: str - """ - addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) - match = re.search(rb"^.*inet6 ([^ ]*) .*$", addresses, flags=re.M) - if match and match.groups(): - return tostr(match.group(1)) - else: - return None - - -def run_iptable_with_check(cmd, *args, **options): - """ - Run an iptables command checking to see if it should: - for --append: run only if rule does not already exist. - for --insert: run only if rule does not already exist. - for --delete: run only if rule does exist. - other commands are run normally. - """ - if "--insert" in args: - check_args = [arg.replace("--insert", "--check") for arg in args] - check_code = run(cmd, *check_args, exitcode=True) - if check_code != 0: - run(cmd, *args, **options) - elif "--append" in args: - check_args = [arg.replace("--append", "--check") for arg in args] - check_code = run(cmd, *check_args, exitcode=True) - if check_code != 0: - run(cmd, *args, **options) - elif "--delete" in args: - check_args = [arg.replace("--delete", "--check") for arg in args] - check_code = run(cmd, *check_args, exitcode=True) - if check_code == 0: - run(cmd, *args, **options) - else: - run(cmd, *args, **options) - - -def iptables(*args, **options): - """ - Run iptables4 and iptables6. - """ - ip4tables(*args, **options) - ip6tables(*args, **options) - - -def ip4tables(*args, **options): - """ - Run iptables4 with checks. - """ - run_iptable_with_check(IPTABLES, *args, **options) - - -def ip6tables(*args, **options): - """ - Run iptables6 with checks. - """ - run_iptable_with_check(IP6TABLES, *args, **options) - - -def toggle_ipv6(status='disable'): - if status == 'disable': - arg = 1 - elif status == 'enable': - arg = 0 - else: - return - cmdcheck([SYSCTL, '-w', 'net.ipv6.conf.all.disable_ipv6=%s' % arg]) - - -# -# NOTE: these tests to see if a chain exists might incorrectly return false. -# This happens when there is an error in calling `iptables --list bitmask`. -# -# For this reason, when stopping the firewall, we do not trust the -# output of ipvx_chain_exists() but instead always attempt to delete -# the chain. -# - - -def ipv4_chain_exists(chain, table=None): - """ - Check if a given chain exists. Only returns true if it actually exists, - but might return false if it exists and iptables failed to run. - - :param chain: the chain to check against - :type chain: str - :rtype: bool - """ - if table is not None: - code = run(IPTABLES, "-t", table, - "--list", chain, "--numeric", exitcode=True) - else: - code = run(IPTABLES, "--list", chain, "--numeric", exitcode=True) - if code == 0: - return True - elif code == 1: - return False - else: - log("ERROR: Could not determine state of iptable chain") - return False - - -def ipv6_chain_exists(chain): - """ - see ipv4_chain_exists() - - :param chain: the chain to check against - :type chain: str - :rtype: bool - """ - code = run(IP6TABLES, "--list", chain, "--numeric", exitcode=True) - if code == 0: - return True - elif code == 1: - return False - else: - log("ERROR: Could not determine state of iptable chain") - return False - - -def enable_ip_forwarding(): - """ - ip_fowarding must be enabled for the firewall to work. - """ - with open('/proc/sys/net/ipv4/ip_forward', 'w') as f: - f.write('1\n') - - -def firewall_start(args): - """ - Bring up the firewall. - - :param args: list of gateways, to be sanitized. - :type args: list - """ - default_device = get_default_device() - local_network_ipv4 = get_local_network_ipv4(default_device) - local_network_ipv6 = get_local_network_ipv6(default_device) - gateways = get_gateways(args) - - # allow local address in listed exception list - # this will allow all ports and both tcp and udp. - def allow4(ip): - ip4tables("--append", BITMASK_CHAIN, "--destination", ip, - "-o", default_device, "--jump", "ACCEPT") - - def allow6(ip): - ip6tables("--append", BITMASK_CHAIN, "--destination", ip, - "-o", default_device, "--jump", "ACCEPT") - - # add custom chain "bitmask" to front of OUTPUT chain for both - # the 'filter' and the 'nat' tables. - if not ipv4_chain_exists(BITMASK_CHAIN): - ip4tables("--new-chain", BITMASK_CHAIN) - if not ipv4_chain_exists(BITMASK_CHAIN_NAT_OUT, 'nat'): - ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_OUT) - if not ipv4_chain_exists(BITMASK_CHAIN_NAT_POST, 'nat'): - ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_POST) - if not ipv6_chain_exists(BITMASK_CHAIN): - ip6tables("--new-chain", BITMASK_CHAIN) - ip4tables("--table", "nat", "--insert", "OUTPUT", - "--jump", BITMASK_CHAIN_NAT_OUT) - ip4tables("--table", "nat", "--insert", "POSTROUTING", - "--jump", BITMASK_CHAIN_NAT_POST) - iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN) - - # route all ipv4 DNS over VPN - # (note: NAT does not work with ipv6 until kernel 3.7) - enable_ip_forwarding() - if QUBES_PROXY and QUBES_VER >= 3: - # rewrite DNS packets for VPN DNS; Qubes preconfigures masquerade - ip4tables("-t", "nat", "--flush", "PR-QBS") - ip4tables("-t", "nat", "--append", "PR-QBS", "-p", "udp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - ip4tables("-t", "nat", "--append", "PR-QBS", "-p", "tcp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - else: - # allow dns to localhost - ip4tables("-t", "nat", "--append", BITMASK_CHAIN, "--protocol", "udp", - "--dest", "127.0.1.1,127.0.0.1,127.0.0.53", "--dport", "53", - "--jump", "ACCEPT") - # rewrite all outgoing packets to use VPN DNS server - # (DNS does sometimes use TCP!) - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "udp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "tcp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - # enable masquerading, so that DNS packets rewritten by DNAT will - # have the correct source IPs. Apply masquerade only to the NAMESERVER, - # we don't want to apply it to the localhost dns resolver. - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, - "--dest", NAMESERVER, - "--protocol", "udp", "--dport", "53", "--jump", "MASQUERADE") - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, - "--dest", NAMESERVER, - "--protocol", "tcp", "--dport", "53", "--jump", "MASQUERADE") - - # allow local network traffic - - ipv4_exceptions = getIPv4AllowAddresses() - if local_network_ipv4: - if len(ipv4_exceptions) == 0: - # allow all local network destinations if no explicit allow rules defined - ip4tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv4, "-o", default_device, - "--jump", "ACCEPT") - # allow local network sources for DNS - # (required to allow local network DNS that gets rewritten by NAT - # to get passed through so that MASQUERADE can set correct source IP) - ip4tables("--append", BITMASK_CHAIN, - "--source", local_network_ipv4, "-o", default_device, - "-p", "udp", "--dport", "53", "--jump", "ACCEPT") - ip4tables("--append", BITMASK_CHAIN, - "--source", local_network_ipv4, "-o", default_device, - "-p", "tcp", "--dport", "53", "--jump", "ACCEPT") - # allow multicast Simple Service Discovery Protocol - ip4tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "239.255.255.250", "--dport", "1900", - "-o", default_device, "--jump", "RETURN") - # allow multicast Bonjour/mDNS - ip4tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "224.0.0.251", "--dport", "5353", - "-o", default_device, "--jump", "RETURN") - - - ipv6_exceptions = getIPv6AllowAddresses() - if local_network_ipv6: - if len(ipv6_exceptions) == 0: - # allow all local network destinations if no explicit allow rules defined - ip6tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv6, "-o", default_device, - "--jump", "ACCEPT") - # allow multicast Simple Service Discovery Protocol - ip6tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "FF05::C", "--dport", "1900", - "-o", default_device, "--jump", "RETURN") - # allow multicast Bonjour/mDNS - ip6tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "FF02::FB", "--dport", "5353", - "-o", default_device, "--jump", "RETURN") - - # allow ipv4 traffic to gateways - for gateway in gateways: - ip4tables("--append", BITMASK_CHAIN, "--destination", gateway, - "-o", default_device, "--jump", "ACCEPT") - - # TODO allow ipv6 traffic to gws too - - # log rejected packets to syslog - if DEBUG: - iptables("--append", BITMASK_CHAIN, "-o", default_device, - "--jump", "LOG", "--log-prefix", "iptables denied: ", - "--log-level", "7") - - # allow explicit private exceptions - if len(ipv4_exceptions) != 0: - for ip in ipv4_exceptions: - allow4(ip) - ip4tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv4, "-o", default_device, - "--jump", "REJECT") - - if len(ipv6_exceptions) != 0: - for ip in ipv6_exceptions: - allow6(ip) - ip6tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv6, "-o", default_device, - "--jump", "REJECT") - - # for now, ensure all other ipv6 packets get rejected (regardless of - # device). not sure why, but "-p any" doesn't work. - ip6tables("--append", BITMASK_CHAIN, "-p", "tcp", "--jump", "REJECT") - ip6tables("--append", BITMASK_CHAIN, "-p", "udp", "--jump", "REJECT") - - # reject all other ipv4 sent over the default device - ip4tables("--append", BITMASK_CHAIN, "-o", - default_device, "--jump", "REJECT") - - - # On Qubes OS, add anti-leak rules for proxyVM qubes-firewall.service - # Must stay on 'top' of chain! - if QUBES_PROXY and QUBES_VER >= 3 and run("grep", "installed\ by\ " + - SCRIPT, QUBES_FW_SCRIPT, - exitcode=True) != 0: - with open(QUBES_FW_SCRIPT, mode="w") as qfile: - qfile.write("#!/bin/sh\n") - qfile.write("# Anti-leak rules installed by " + SCRIPT + " " - + VERSION + "\n") - qfile.write("iptables --insert FORWARD -i eth0 -j DROP\n") - qfile.write("iptables --insert FORWARD -o eth0 -j DROP\n") - qfile.write("ip6tables --insert FORWARD -i eth0 -j DROP\n") - qfile.write("ip6tables --insert FORWARD -o eth0 -j DROP\n") - qfile.write("iptables --insert INPUT -i tun+ -j DROP\n") - qfile.write("ip6tables --insert INPUT -i tun+ -j DROP\n") - os.chmod(QUBES_FW_SCRIPT, stat.S_IRWXU) - if not os.path.exists(QUBES_IPHOOK): - os.symlink(QUBES_FW_SCRIPT, QUBES_IPHOOK) - if QUBES_VER == 4: - run(QUBES_FW_SCRIPT) - elif QUBES_VER == 3: - run("systemctl", "restart", "qubes-firewall.service") - - # toggle_ipv6('disable') - - -def firewall_stop(): - """ - Stop the firewall. Because we really really always want the firewall to - be stopped if at all possible, this function is cautious and contains a - lot of trys and excepts. - - If there were any problems, we raise an exception at the end. This allows - the calling code to retry stopping the firewall. Stopping the firewall - can fail if iptables is being run by another process (only one iptables - command can be run at a time). - """ - ok = True - - # -t filter -D OUTPUT -j bitmask - try: - iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to remove bitmask firewall from OUTPUT chain " - "(maybe it is already removed?)", exc) - ok = False - - # -t nat -D OUTPUT -j bitmask - try: - ip4tables("-t", "nat", "--delete", "OUTPUT", - "--jump", BITMASK_CHAIN_NAT_OUT, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to remove bitmask firewall from OUTPUT chain " - "in 'nat' table (maybe it is already removed?)", exc) - ok = False - - # -t nat -D POSTROUTING -j bitmask_postrouting - try: - ip4tables("-t", "nat", "--delete", "POSTROUTING", - "--jump", BITMASK_CHAIN_NAT_POST, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to remove bitmask firewall from POSTROUTING " - "chain in 'nat' table (maybe it is already removed?)", exc) - ok = False - - # -t filter --delete-chain bitmask - try: - ip4tables("--flush", BITMASK_CHAIN, throw=True) - ip4tables("--delete-chain", BITMASK_CHAIN, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv4 firewall " - "chain (maybe it is already destroyed?)", exc) - ok = False - - # -t nat --delete-chain bitmask - try: - ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_OUT, throw=True) - ip4tables("-t", "nat", "--delete-chain", - BITMASK_CHAIN_NAT_OUT, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv4 firewall " - "chain in 'nat' table (maybe it is already destroyed?)", exc) - ok = False - - # -t nat --delete-chain bitmask_postrouting - try: - ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_POST, throw=True) - ip4tables("-t", "nat", "--delete-chain", - BITMASK_CHAIN_NAT_POST, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv4 firewall " - "chain in 'nat' table (maybe it is already destroyed?)", exc) - ok = False - - # -t filter --delete-chain bitmask (ipv6) - try: - ip6tables("--flush", BITMASK_CHAIN, throw=True) - ip6tables("--delete-chain", BITMASK_CHAIN, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv6 firewall " - "chain (maybe it is already destroyed?)", exc) - ok = False - - # toggle_ipv6('enable') - - if not (ok or ipv4_chain_exists or ipv6_chain_exists): - raise Exception("firewall might still be left up. " - "Please try `firewall stop` again.") - - -# -# MAIN -# - -USAGE = """ -This is bitmask-root version {VERSION} - -This program manipulates the Bitmask firewall. It is *not* intented to be used -manually. - -Commands: - -{SCRIPT} version -{SCRIPT} restart -{SCRIPT} openvpn start -{SCRIPT} openvpn stop -{SCRIPT} firewall start -{SCRIPT} firewall stop -{SCRIPT} firewall isup -""".format(SCRIPT=SCRIPT, VERSION=VERSION) - - -def main(): - """ - Entry point for cmdline execution. - """ - # TODO use argparse instead please. - - if len(sys.argv) >= 2: - command = "_".join(sys.argv[1:3]) - args = sys.argv[3:] - - is_restart = False - - if command == 'help' or command == '-h': - print(USAGE) - exit(0) - - if args and args[0] == 'restart': - is_restart = True - args.remove('restart') - - if command == "version": - print(VERSION) - exit(0) - - if os.getuid() != 0: - bail("ERROR: must be run as root") - - if command == "openvpn_start": - openvpn_start(args) - - elif command == "openvpn_stop": - openvpn_stop(args) - - elif command == "firewall_start": - try: - firewall_start(args) - except Exception as ex: - if not is_restart: - firewall_stop() - bail("ERROR: could not start firewall", ex) - - elif command == "firewall_stop": - try: - firewall_stop() - except Exception as ex: - bail("ERROR: could not stop firewall", ex) - - elif command == "firewall_isup": - if ipv4_chain_exists(BITMASK_CHAIN): - # too verbose since bitmask polls this - pass - else: - bail("INFO: bitmask firewall is down") - - else: - bail("ERROR: No such command. Try bitmask-root help") - else: - bail("ERROR: No such command. Try bitmask-root help") - - -if __name__ == "__main__": - debug(" ".join(sys.argv)) - main() - exit(0) diff --git a/helpers/se.leap.bitmask.policy b/helpers/se.leap.bitmask.policy deleted file mode 100644 index c1def93..0000000 --- a/helpers/se.leap.bitmask.policy +++ /dev/null @@ -1,23 +0,0 @@ - - - - - LEAP Encryption Access Project - http://leap.se/ - - - Runs bitmask helper to launch firewall and openvpn - Ejecuta el asistente de bitmask para lanzar el firewall y openvpn - Bitmask needs that you authenticate to start - Bitmask necesita autorizacion para comenzar - package-x-generic - - yes - yes - yes - - /usr/sbin/bitmask-root - - diff --git a/helpers/se.leap.bitmask.snap.policy b/helpers/se.leap.bitmask.snap.policy deleted file mode 100644 index cac56b4..0000000 --- a/helpers/se.leap.bitmask.snap.policy +++ /dev/null @@ -1,23 +0,0 @@ - - - - - LEAP Encryption Access Project Project - http://leap.se/ - - - Runs bitmask helper to launch firewall and openvpn (${applicationName}) - Ejecuta el asistente de bitmask para lanzar el firewall y openvpn (${applicationName}) - ${applicationName} needs that you authenticate to start - ${applicationName} necesita autorizacion para comenzar - package-x-generic - - yes - yes - yes - - /snap/bin/${binaryName}.bitmask-root - - diff --git a/pkg/pickle/helpers.go b/pkg/pickle/helpers.go index 69d0e87..c0bd024 100644 --- a/pkg/pickle/helpers.go +++ b/pkg/pickle/helpers.go @@ -1,5 +1,3 @@ -//go:generate statik -src=../../helpers -include=* - // Copyright (C) 2020 LEAP // // This program is free software: you can redistribute it and/or modify @@ -18,17 +16,19 @@ package pickle import ( + "embed" "fmt" - "io/ioutil" + "io" "log" "os" "os/exec" + "path" "runtime" - - _ "0xacab.org/leap/bitmask-vpn/pkg/pickle/statik" - "github.com/rakyll/statik/fs" ) +//go:embed helpers +var helpers embed.FS + const ( bitmaskRoot = "/usr/sbin/bitmask-root" // TODO parametrize this with config.appName @@ -96,27 +96,17 @@ func dumpHelper(fname, dest string, isExec bool) { fmt.Println("Only linux supported for now") return } - stFS, err := fs.New() - if err != nil { - log.Fatal(err) - } - - r, err := stFS.Open("/" + fname) - if err != nil { - log.Fatal(err) - } - defer r.Close() - c, err := ioutil.ReadAll(r) + fd, err := helpers.Open(path.Join("helpers", fname)) if err != nil { log.Fatal(err) } - tmpfile, err := ioutil.TempFile("/dev/shm", "*") + tmpfile, err := os.CreateTemp("/dev/shm", "*") check(err) defer os.Remove(tmpfile.Name()) - _, err = tmpfile.Write(c) + _, err = io.Copy(tmpfile, fd) check(err) copyAsRoot(tmpfile.Name(), dest, isExec) } diff --git a/pkg/pickle/helpers/bitmask-root b/pkg/pickle/helpers/bitmask-root new file mode 100644 index 0000000..d33091c --- /dev/null +++ b/pkg/pickle/helpers/bitmask-root @@ -0,0 +1,1038 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014-2019 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +""" +This is a privileged helper script for safely running certain commands as root. +It should only be called by the Bitmask application. + +Expected paths: + + When installed by distro path: + /usr/sbin/bitmask-root + + When installed by bundle or from git: + /usr/local/sbin/bitmask-root + + When installed by snap: + /snap/bin/riseup-vpn.bitmask-root + +USAGE: + bitmask-root firewall stop + bitmask-root firewall start [restart] GATEWAY1 GATEWAY2 ... + bitmask-root openvpn stop + bitmask-root openvpn start CONFIG1 CONFIG1 ... + +All actions return exit code 0 for success, non-zero otherwise. + +The `openvpn start` action is special: it calls exec on openvpn and replaces +the current process. If the `restart` parameter is passed, the firewall will +not be teared down in the case of an error during launch. +""" +import ipaddress +import os +import re +import signal +import socket +import syslog +import subprocess +import sys +import stat +import traceback + +cmdcheck = subprocess.check_output + +# +# CONSTANTS + +def get_no_group_name(): + """ + Return the right group name to use for the current OS. + Examples: + - Ubuntu: nogroup + - Arch: nobody + + :rtype: str or None + """ + import grp + try: + grp.getgrnam('nobody') + return 'nobody' + except KeyError: + try: + grp.getgrnam('nogroup') + return 'nogroup' + except KeyError: + return None + +def is_ipv6_disabled(): + """ + Return True if ipv6 support is disabled by the kernel. + """ + code = os.system("sysctl -a 2>/dev/null | grep all.disable_ipv6 | grep 1") + return code == 0 + +def tostr(s): + return s.decode('utf-8') + +VERSION = "16" +SCRIPT = "bitmask-root" +NAMESERVER_TCP = "10.41.0.1" +NAMESERVER_UDP = "10.42.0.1" + +if os.getenv("UDP") == "1": + NAMESERVER = NAMESERVER_UDP +else: + NAMESERVER = NAMESERVER_TCP +BITMASK_CHAIN = "bitmask" +BITMASK_CHAIN_NAT_OUT = "bitmask" +BITMASK_CHAIN_NAT_POST = "bitmask_postrouting" +LOCAL_INTERFACE = "lo" + +def swhich(binary): + """ + Find the path to binary in sbin + + :rtype: str + """ + for folder in ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/local/sbin"]: + path = os.path.join(folder, binary) + if os.path.isfile(path): + return path + + raise Exception("Can't find %s" % (binary,)) + +IP = swhich("ip") +IPTABLES = swhich("iptables") +IP6TABLES = swhich("ip6tables") +SYSCTL = swhich("sysctl") + +OPENVPN_USER = "nobody" +OPENVPN_GROUP = get_no_group_name() +LEAPOPENVPN = "LEAPOPENVPN" +OPENVPN_SYSTEM_BIN = "/usr/sbin/openvpn" # Debian location +OPENVPN_LEAP_BIN = "/usr/local/sbin/leap-openvpn" # installed by bundle +OPENVPN_SNAP_BIN = "/snap/bin/riseup-vpn.openvpn" # installed by snap + +FIXED_FLAGS = [ + "--setenv", "LEAPOPENVPN", "1", + "--nobind", + "--client", + "--dev", "tun", + "--tls-client", + "--remote-cert-tls", "server", + "--management-signal", + "--script-security", "1", + "--user", "nobody", + "--persist-key", + "--persist-local-ip", + "--tls-version-min", "1.0", +] + +if OPENVPN_GROUP is not None: + FIXED_FLAGS.extend(["--group", OPENVPN_GROUP]) + +if is_ipv6_disabled(): + FIXED_FLAGS.extend([ + "--pull-filter", "ignore", "ifconfig-ipv6", + "--pull-filter", "ignore", "route-ipv6"]) + +ALLOWED_FLAGS = { + "--remote": ["IP", "NUMBER", "PROTO"], + "--tls-cipher": ["CIPHER"], + "--cipher": ["CIPHER"], + "--auth": ["CIPHER"], + "--management": ["DIR||IP", "UNIXSOCKET||NUMBER"], + "--management-client-user": ["USER"], + "--route": ["IP", "IP", "NETGW"], + "--cert": ["FILE"], + "--key": ["FILE"], + "--ca": ["FILE"], + "--fragment": ["NUMBER"], + "--keepalive": ["NUMBER", "NUMBER"], + "--verb": ["NUMBER"], + "--management-client": [], + "--tun-ipv6": [], + "--log": ["LOGFILE"], +} + +PARAM_FORMATS = { + "NUMBER": lambda s: re.match("^\d+$", s), + "PROTO": lambda s: re.match("^(tcp|udp|tcp4|udp4)$", s), + "IP": lambda s: is_valid_address(s), + "CIPHER": lambda s: re.match("^[A-Z0-9-]+$", s), + "USER": lambda s: re.match( + "^[a-zA-Z0-9_\.\@][a-zA-Z0-9_\-\.\@]*\$?$", s), # IEEE Std 1003.1-2001 + "FILE": lambda s: os.path.isfile(s), + "DIR": lambda s: os.path.isdir(os.path.split(s)[0]), + "UNIXSOCKET": lambda s: s == "unix", + "NETGW": lambda s: s == "net_gateway", + "UID": lambda s: re.match("^[a-zA-Z0-9]+$", s), + "LOGFILE": lambda s: s == "/tmp/leap-vpn.log", +} + +# Determine Qubes OS version, if any +QUBES_PROXY = os.path.exists("/var/run/qubes/this-is-proxyvm") +if os.path.isdir("/etc/qubes"): + QUBES_CFG = "/rw/config/" + QUBES_IPHOOK = QUBES_CFG + "qubes-ip-change-hook" + QUBES_FW_SCRIPT = QUBES_CFG + "qubes-firewall-user-script" + if subprocess.call([IPTABLES, "--list", "QBS-FORWARD"]) == 0: + QUBES_VER = 4 + else: + QUBES_VER = 3 +else: + # not a Qubes system + QUBES_VER = 0 + + +DEBUG = os.getenv("DEBUG") +TEST = os.getenv("TEST") + +if DEBUG: + import logging + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + ch.setFormatter(formatter) + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + logger.addHandler(ch) + +syslog.openlog(SCRIPT) + +# +# UTILITY +# + +def is_valid_address(value): + """ + Validate that the passed ip is a valid IP address. + + :param value: the value to be validated + :type value: str + :rtype: bool + """ + try: + socket.inet_aton(value) + return True + except Exception: + log("%s: ERROR: MALFORMED IP: %s!" % (SCRIPT, value)) + return False + + +def split_list(_list, regex): + """ + Split a list based on a regex: + e.g. split_list(["xx", "yy", "x1", "zz"], "^x") => [["xx", "yy"], ["x1", + "zz"]] + + :param _list: the list to be split. + :type _list: list + :param regex: the regex expression to filter with. + :type regex: str + + :rtype: list + """ + if not hasattr(regex, "match"): + regex = re.compile(regex) + result = [] + i = 0 + if not _list: + return result + while True: + if regex.match(_list[i]): + result.append([]) + while True: + result[-1].append(_list[i]) + i += 1 + if i >= len(_list) or regex.match(_list[i]): + break + else: + i += 1 + if i >= len(_list): + break + return result + + +def get_process_list(): + """ + Get a process list by reading `/proc` filesystem. + + :return: a list of tuples, each containing pid and command string. + :rtype: tuple if lists + """ + res = [] + pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] + + for pid in pids: + try: + res.append((pid, open( + os.path.join( + '/proc', pid, 'cmdline'), 'rb').read())) + except IOError: # proc has already terminated + continue + return filter(None, res) + + +def getIPv4AllowAddresses(): + lines = [] + try: + with open("/etc/bitmask/ipv4.allow", 'r') as f: + lines = [l.strip() for l in f.readlines()] + except FileNotFoundError: + return lines + + lines = filter(lambda x: ipaddress.ip_address(x).version == 4, lines) + return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) + +def getIPv6AllowAddresses(): + lines = [] + try: + with open("/etc/bitmask/ipv6.allow", 'r') as f: + lines = [l.strip() for l in f.readlines()] + except FileNotFoundError: + return lines + + lines = filter(lambda x: ipaddress.ip_address(x).version == 6, lines) + return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) + + +def run(command, *args, **options): + """ + Run an external command. + + Options: + + `check`: If True, check the command's output. bail if non-zero. (the + default is true unless detach or input is true) + `exitcode`: like `check`, but return exitcode instead of bailing. + `detach`: If True, run in detached process. + `input`: If True, open command for writing stream to, returning the Popen + object. + `throw`: If True, raise an exception if there is an error instead + of bailing. + """ + parts = [command] + parts.extend(args) + debug("%s run: %s " % (SCRIPT, " ".join(parts))) + + _check = options.get("check", True) + _detach = options.get("detach", False) + _input = options.get("input", False) + _exitcode = options.get("exitcode", False) + _throw = options.get("throw", False) + + if not (_check or _throw) or _detach or _input: + if _input: + return subprocess.Popen(parts, stdin=subprocess.PIPE) + else: + subprocess.Popen(parts) + return None + else: + try: + devnull = open('/dev/null', 'w') + subprocess.check_call(parts, stdout=devnull, stderr=devnull) + return 0 + except subprocess.CalledProcessError as exc: + if _exitcode: + if exc.returncode != 1: + # 0 or 1 is to be expected, but anything else + # should be logged. + debug("ERROR: Could not run %s: %s" % + (exc.cmd, exc.output), exception=exc) + return exc.returncode + elif _throw: + raise exc + else: + bail("ERROR: Could not run %s: %s" % (exc.cmd, exc.output), + exception=exc) + + +def log(msg=None, exception=None, priority=syslog.LOG_INFO): + """ + print and log warning message or exception. + + :param msg: optional error message. + :type msg: str + :param msg: optional exception. + :type msg: Exception + :param msg: syslog level + :type msg: one of LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, + LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG + """ + if msg is not None: + print("%s: %s" % (SCRIPT, msg)) + syslog.syslog(priority, msg) + if exception is not None: + if TEST or DEBUG: + traceback.print_exc() + syslog.syslog(priority, traceback.format_exc()) + + +def debug(msg=None, exception=None): + """ + Just like log, but is skipped unless DEBUG. Use syslog.LOG_INFO + even for debug messages (we don't want to miss them). + """ + if TEST or DEBUG: + log(msg, exception) + + +def bail(msg=None, exception=None): + """ + abnormal exit. like log(), but exits with error status code. + """ + log(msg, exception) + exit(1) + +# +# OPENVPN +# + + +def get_openvpn_bin(): + """ + Return the path for either the system openvpn or the one the + bundle has put there. + """ + if os.environ.get('SNAP') and os.path.isfile(OPENVPN_SNAP_BIN): + # the snap option should be removed from the debian package. + return OPENVPN_SNAP_BIN + + if os.path.isfile(OPENVPN_SYSTEM_BIN): + return OPENVPN_SYSTEM_BIN + + # the bundle option should also be removed from the debian. + if os.path.isfile(OPENVPN_LEAP_BIN): + return OPENVPN_LEAP_BIN + + +def parse_openvpn_flags(args): + """ + Take argument list from the command line and parse it, only allowing some + configuration flags. + + :type args: list + """ + result = [] + try: + for flag in split_list(args, "^--"): + flag_name = flag[0] + if flag_name in ALLOWED_FLAGS: + result.append(flag_name) + required_params = ALLOWED_FLAGS[flag_name] + if required_params: + # flatten if separated by spaces + flag_params = [i for sublist in map( + lambda s: s.split(), flag[1:]) for i in sublist] + if len(flag_params) != len(required_params): + log("%s: ERROR: not enough params for %s" % + (SCRIPT, flag_name)) + return None + for param, param_type in zip(flag_params, required_params): + for tpe in param_type.split("||"): + if PARAM_FORMATS[tpe](param): + result.append(param) + break + else: + log("%s: ERROR: Bad argument %s" % + (SCRIPT, param)) + return None + else: + log("WARNING: unrecognized openvpn flag %s" % flag_name) + return result + except Exception as exc: + log("%s: ERROR PARSING FLAGS: %s" % (SCRIPT, exc)) + if DEBUG: + logger.exception(exc) + return None + + +def openvpn_start(args): + """ + Launch openvpn, sanitizing input, and replacing the current process with + the openvpn process. + + :param args: arguments to be passed to openvpn + :type args: list + """ + openvpn_flags = parse_openvpn_flags(args) + if openvpn_flags: + OPENVPN = get_openvpn_bin() + flags = [OPENVPN] + FIXED_FLAGS + openvpn_flags + if DEBUG: + log("%s: running openvpn with flags:" % (SCRIPT,)) + log(flags) + # note: first argument to command is ignored, but customarily set to + # the command. + os.execv(OPENVPN, flags) + else: + bail('ERROR: could not parse openvpn options') + + +def openvpn_stop(args): + """ + Stop the openvpn that has likely been launched by bitmask. + + :param args: arguments to openvpn + :type args: list + """ + plist = get_process_list() + for pid, proc in plist: + if bytes("openvpn", 'utf-8') in proc and bytes(LEAPOPENVPN, 'utf-8') in proc: + os.kill(int(pid), signal.SIGTERM) + break + +# +# FIREWALL +# + + +def get_gateways(gateways): + """ + Filter a passed sequence of gateways, returning only the valid ones. + + :param gateways: a sequence of gateways to filter. + :type gateways: iterable + :rtype: iterable + """ + result = filter(is_valid_address, gateways) + if not result: + bail("ERROR: No valid gateways specified") + else: + return result + + +def get_default_device(): + """ + Retrieve the current default network device. + + :rtype: str + """ + routes = subprocess.check_output([IP, "route", "show"]) + match = re.search(rb"^default .*dev ([^\s]*) .*$", routes, flags=re.M) + if match and match.groups(): + return tostr(match.group(1)) + else: + bail("Could not find default device") + + +def get_local_network_ipv4(device): + """ + Get the local ipv4 addres for a given device. + + :param device: + :type device: str + """ + addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) + match = re.search(rb"^.*inet ([^ ]*) .*$", addresses, flags=re.M) + if match and match.groups(): + return tostr(match.group(1)) + else: + return None + + +def get_local_network_ipv6(device): + """ + Get the local ipv6 addres for a given device. + + :param device: + :type device: str + """ + addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) + match = re.search(rb"^.*inet6 ([^ ]*) .*$", addresses, flags=re.M) + if match and match.groups(): + return tostr(match.group(1)) + else: + return None + + +def run_iptable_with_check(cmd, *args, **options): + """ + Run an iptables command checking to see if it should: + for --append: run only if rule does not already exist. + for --insert: run only if rule does not already exist. + for --delete: run only if rule does exist. + other commands are run normally. + """ + if "--insert" in args: + check_args = [arg.replace("--insert", "--check") for arg in args] + check_code = run(cmd, *check_args, exitcode=True) + if check_code != 0: + run(cmd, *args, **options) + elif "--append" in args: + check_args = [arg.replace("--append", "--check") for arg in args] + check_code = run(cmd, *check_args, exitcode=True) + if check_code != 0: + run(cmd, *args, **options) + elif "--delete" in args: + check_args = [arg.replace("--delete", "--check") for arg in args] + check_code = run(cmd, *check_args, exitcode=True) + if check_code == 0: + run(cmd, *args, **options) + else: + run(cmd, *args, **options) + + +def iptables(*args, **options): + """ + Run iptables4 and iptables6. + """ + ip4tables(*args, **options) + ip6tables(*args, **options) + + +def ip4tables(*args, **options): + """ + Run iptables4 with checks. + """ + run_iptable_with_check(IPTABLES, *args, **options) + + +def ip6tables(*args, **options): + """ + Run iptables6 with checks. + """ + run_iptable_with_check(IP6TABLES, *args, **options) + + +def toggle_ipv6(status='disable'): + if status == 'disable': + arg = 1 + elif status == 'enable': + arg = 0 + else: + return + cmdcheck([SYSCTL, '-w', 'net.ipv6.conf.all.disable_ipv6=%s' % arg]) + + +# +# NOTE: these tests to see if a chain exists might incorrectly return false. +# This happens when there is an error in calling `iptables --list bitmask`. +# +# For this reason, when stopping the firewall, we do not trust the +# output of ipvx_chain_exists() but instead always attempt to delete +# the chain. +# + + +def ipv4_chain_exists(chain, table=None): + """ + Check if a given chain exists. Only returns true if it actually exists, + but might return false if it exists and iptables failed to run. + + :param chain: the chain to check against + :type chain: str + :rtype: bool + """ + if table is not None: + code = run(IPTABLES, "-t", table, + "--list", chain, "--numeric", exitcode=True) + else: + code = run(IPTABLES, "--list", chain, "--numeric", exitcode=True) + if code == 0: + return True + elif code == 1: + return False + else: + log("ERROR: Could not determine state of iptable chain") + return False + + +def ipv6_chain_exists(chain): + """ + see ipv4_chain_exists() + + :param chain: the chain to check against + :type chain: str + :rtype: bool + """ + code = run(IP6TABLES, "--list", chain, "--numeric", exitcode=True) + if code == 0: + return True + elif code == 1: + return False + else: + log("ERROR: Could not determine state of iptable chain") + return False + + +def enable_ip_forwarding(): + """ + ip_fowarding must be enabled for the firewall to work. + """ + with open('/proc/sys/net/ipv4/ip_forward', 'w') as f: + f.write('1\n') + + +def firewall_start(args): + """ + Bring up the firewall. + + :param args: list of gateways, to be sanitized. + :type args: list + """ + default_device = get_default_device() + local_network_ipv4 = get_local_network_ipv4(default_device) + local_network_ipv6 = get_local_network_ipv6(default_device) + gateways = get_gateways(args) + + # allow local address in listed exception list + # this will allow all ports and both tcp and udp. + def allow4(ip): + ip4tables("--append", BITMASK_CHAIN, "--destination", ip, + "-o", default_device, "--jump", "ACCEPT") + + def allow6(ip): + ip6tables("--append", BITMASK_CHAIN, "--destination", ip, + "-o", default_device, "--jump", "ACCEPT") + + # add custom chain "bitmask" to front of OUTPUT chain for both + # the 'filter' and the 'nat' tables. + if not ipv4_chain_exists(BITMASK_CHAIN): + ip4tables("--new-chain", BITMASK_CHAIN) + if not ipv4_chain_exists(BITMASK_CHAIN_NAT_OUT, 'nat'): + ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_OUT) + if not ipv4_chain_exists(BITMASK_CHAIN_NAT_POST, 'nat'): + ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_POST) + if not ipv6_chain_exists(BITMASK_CHAIN): + ip6tables("--new-chain", BITMASK_CHAIN) + ip4tables("--table", "nat", "--insert", "OUTPUT", + "--jump", BITMASK_CHAIN_NAT_OUT) + ip4tables("--table", "nat", "--insert", "POSTROUTING", + "--jump", BITMASK_CHAIN_NAT_POST) + iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN) + + # route all ipv4 DNS over VPN + # (note: NAT does not work with ipv6 until kernel 3.7) + enable_ip_forwarding() + if QUBES_PROXY and QUBES_VER >= 3: + # rewrite DNS packets for VPN DNS; Qubes preconfigures masquerade + ip4tables("-t", "nat", "--flush", "PR-QBS") + ip4tables("-t", "nat", "--append", "PR-QBS", "-p", "udp", + "--dport", "53", "--jump", "DNAT", "--to", + NAMESERVER + ":53") + ip4tables("-t", "nat", "--append", "PR-QBS", "-p", "tcp", + "--dport", "53", "--jump", "DNAT", "--to", + NAMESERVER + ":53") + else: + # allow dns to localhost + ip4tables("-t", "nat", "--append", BITMASK_CHAIN, "--protocol", "udp", + "--dest", "127.0.1.1,127.0.0.1,127.0.0.53", "--dport", "53", + "--jump", "ACCEPT") + # rewrite all outgoing packets to use VPN DNS server + # (DNS does sometimes use TCP!) + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "udp", + "--dport", "53", "--jump", "DNAT", "--to", + NAMESERVER + ":53") + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "tcp", + "--dport", "53", "--jump", "DNAT", "--to", + NAMESERVER + ":53") + # enable masquerading, so that DNS packets rewritten by DNAT will + # have the correct source IPs. Apply masquerade only to the NAMESERVER, + # we don't want to apply it to the localhost dns resolver. + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, + "--dest", NAMESERVER, + "--protocol", "udp", "--dport", "53", "--jump", "MASQUERADE") + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, + "--dest", NAMESERVER, + "--protocol", "tcp", "--dport", "53", "--jump", "MASQUERADE") + + # allow local network traffic + + ipv4_exceptions = getIPv4AllowAddresses() + if local_network_ipv4: + if len(ipv4_exceptions) == 0: + # allow all local network destinations if no explicit allow rules defined + ip4tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv4, "-o", default_device, + "--jump", "ACCEPT") + # allow local network sources for DNS + # (required to allow local network DNS that gets rewritten by NAT + # to get passed through so that MASQUERADE can set correct source IP) + ip4tables("--append", BITMASK_CHAIN, + "--source", local_network_ipv4, "-o", default_device, + "-p", "udp", "--dport", "53", "--jump", "ACCEPT") + ip4tables("--append", BITMASK_CHAIN, + "--source", local_network_ipv4, "-o", default_device, + "-p", "tcp", "--dport", "53", "--jump", "ACCEPT") + # allow multicast Simple Service Discovery Protocol + ip4tables("--append", BITMASK_CHAIN, + "--protocol", "udp", + "--destination", "239.255.255.250", "--dport", "1900", + "-o", default_device, "--jump", "RETURN") + # allow multicast Bonjour/mDNS + ip4tables("--append", BITMASK_CHAIN, + "--protocol", "udp", + "--destination", "224.0.0.251", "--dport", "5353", + "-o", default_device, "--jump", "RETURN") + + + ipv6_exceptions = getIPv6AllowAddresses() + if local_network_ipv6: + if len(ipv6_exceptions) == 0: + # allow all local network destinations if no explicit allow rules defined + ip6tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv6, "-o", default_device, + "--jump", "ACCEPT") + # allow multicast Simple Service Discovery Protocol + ip6tables("--append", BITMASK_CHAIN, + "--protocol", "udp", + "--destination", "FF05::C", "--dport", "1900", + "-o", default_device, "--jump", "RETURN") + # allow multicast Bonjour/mDNS + ip6tables("--append", BITMASK_CHAIN, + "--protocol", "udp", + "--destination", "FF02::FB", "--dport", "5353", + "-o", default_device, "--jump", "RETURN") + + # allow ipv4 traffic to gateways + for gateway in gateways: + ip4tables("--append", BITMASK_CHAIN, "--destination", gateway, + "-o", default_device, "--jump", "ACCEPT") + + # TODO allow ipv6 traffic to gws too + + # log rejected packets to syslog + if DEBUG: + iptables("--append", BITMASK_CHAIN, "-o", default_device, + "--jump", "LOG", "--log-prefix", "iptables denied: ", + "--log-level", "7") + + # allow explicit private exceptions + if len(ipv4_exceptions) != 0: + for ip in ipv4_exceptions: + allow4(ip) + ip4tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv4, "-o", default_device, + "--jump", "REJECT") + + if len(ipv6_exceptions) != 0: + for ip in ipv6_exceptions: + allow6(ip) + ip6tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv6, "-o", default_device, + "--jump", "REJECT") + + # for now, ensure all other ipv6 packets get rejected (regardless of + # device). not sure why, but "-p any" doesn't work. + ip6tables("--append", BITMASK_CHAIN, "-p", "tcp", "--jump", "REJECT") + ip6tables("--append", BITMASK_CHAIN, "-p", "udp", "--jump", "REJECT") + + # reject all other ipv4 sent over the default device + ip4tables("--append", BITMASK_CHAIN, "-o", + default_device, "--jump", "REJECT") + + + # On Qubes OS, add anti-leak rules for proxyVM qubes-firewall.service + # Must stay on 'top' of chain! + if QUBES_PROXY and QUBES_VER >= 3 and run("grep", "installed\ by\ " + + SCRIPT, QUBES_FW_SCRIPT, + exitcode=True) != 0: + with open(QUBES_FW_SCRIPT, mode="w") as qfile: + qfile.write("#!/bin/sh\n") + qfile.write("# Anti-leak rules installed by " + SCRIPT + " " + + VERSION + "\n") + qfile.write("iptables --insert FORWARD -i eth0 -j DROP\n") + qfile.write("iptables --insert FORWARD -o eth0 -j DROP\n") + qfile.write("ip6tables --insert FORWARD -i eth0 -j DROP\n") + qfile.write("ip6tables --insert FORWARD -o eth0 -j DROP\n") + qfile.write("iptables --insert INPUT -i tun+ -j DROP\n") + qfile.write("ip6tables --insert INPUT -i tun+ -j DROP\n") + os.chmod(QUBES_FW_SCRIPT, stat.S_IRWXU) + if not os.path.exists(QUBES_IPHOOK): + os.symlink(QUBES_FW_SCRIPT, QUBES_IPHOOK) + if QUBES_VER == 4: + run(QUBES_FW_SCRIPT) + elif QUBES_VER == 3: + run("systemctl", "restart", "qubes-firewall.service") + + # toggle_ipv6('disable') + + +def firewall_stop(): + """ + Stop the firewall. Because we really really always want the firewall to + be stopped if at all possible, this function is cautious and contains a + lot of trys and excepts. + + If there were any problems, we raise an exception at the end. This allows + the calling code to retry stopping the firewall. Stopping the firewall + can fail if iptables is being run by another process (only one iptables + command can be run at a time). + """ + ok = True + + # -t filter -D OUTPUT -j bitmask + try: + iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to remove bitmask firewall from OUTPUT chain " + "(maybe it is already removed?)", exc) + ok = False + + # -t nat -D OUTPUT -j bitmask + try: + ip4tables("-t", "nat", "--delete", "OUTPUT", + "--jump", BITMASK_CHAIN_NAT_OUT, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to remove bitmask firewall from OUTPUT chain " + "in 'nat' table (maybe it is already removed?)", exc) + ok = False + + # -t nat -D POSTROUTING -j bitmask_postrouting + try: + ip4tables("-t", "nat", "--delete", "POSTROUTING", + "--jump", BITMASK_CHAIN_NAT_POST, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to remove bitmask firewall from POSTROUTING " + "chain in 'nat' table (maybe it is already removed?)", exc) + ok = False + + # -t filter --delete-chain bitmask + try: + ip4tables("--flush", BITMASK_CHAIN, throw=True) + ip4tables("--delete-chain", BITMASK_CHAIN, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv4 firewall " + "chain (maybe it is already destroyed?)", exc) + ok = False + + # -t nat --delete-chain bitmask + try: + ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_OUT, throw=True) + ip4tables("-t", "nat", "--delete-chain", + BITMASK_CHAIN_NAT_OUT, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv4 firewall " + "chain in 'nat' table (maybe it is already destroyed?)", exc) + ok = False + + # -t nat --delete-chain bitmask_postrouting + try: + ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_POST, throw=True) + ip4tables("-t", "nat", "--delete-chain", + BITMASK_CHAIN_NAT_POST, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv4 firewall " + "chain in 'nat' table (maybe it is already destroyed?)", exc) + ok = False + + # -t filter --delete-chain bitmask (ipv6) + try: + ip6tables("--flush", BITMASK_CHAIN, throw=True) + ip6tables("--delete-chain", BITMASK_CHAIN, throw=True) + except subprocess.CalledProcessError as exc: + debug("INFO: not able to flush and delete bitmask ipv6 firewall " + "chain (maybe it is already destroyed?)", exc) + ok = False + + # toggle_ipv6('enable') + + if not (ok or ipv4_chain_exists or ipv6_chain_exists): + raise Exception("firewall might still be left up. " + "Please try `firewall stop` again.") + + +# +# MAIN +# + +USAGE = """ +This is bitmask-root version {VERSION} + +This program manipulates the Bitmask firewall. It is *not* intented to be used +manually. + +Commands: + +{SCRIPT} version +{SCRIPT} restart +{SCRIPT} openvpn start +{SCRIPT} openvpn stop +{SCRIPT} firewall start +{SCRIPT} firewall stop +{SCRIPT} firewall isup +""".format(SCRIPT=SCRIPT, VERSION=VERSION) + + +def main(): + """ + Entry point for cmdline execution. + """ + # TODO use argparse instead please. + + if len(sys.argv) >= 2: + command = "_".join(sys.argv[1:3]) + args = sys.argv[3:] + + is_restart = False + + if command == 'help' or command == '-h': + print(USAGE) + exit(0) + + if args and args[0] == 'restart': + is_restart = True + args.remove('restart') + + if command == "version": + print(VERSION) + exit(0) + + if os.getuid() != 0: + bail("ERROR: must be run as root") + + if command == "openvpn_start": + openvpn_start(args) + + elif command == "openvpn_stop": + openvpn_stop(args) + + elif command == "firewall_start": + try: + firewall_start(args) + except Exception as ex: + if not is_restart: + firewall_stop() + bail("ERROR: could not start firewall", ex) + + elif command == "firewall_stop": + try: + firewall_stop() + except Exception as ex: + bail("ERROR: could not stop firewall", ex) + + elif command == "firewall_isup": + if ipv4_chain_exists(BITMASK_CHAIN): + # too verbose since bitmask polls this + pass + else: + bail("INFO: bitmask firewall is down") + + else: + bail("ERROR: No such command. Try bitmask-root help") + else: + bail("ERROR: No such command. Try bitmask-root help") + + +if __name__ == "__main__": + debug(" ".join(sys.argv)) + main() + exit(0) diff --git a/pkg/pickle/helpers/se.leap.bitmask.policy b/pkg/pickle/helpers/se.leap.bitmask.policy new file mode 100644 index 0000000..c1def93 --- /dev/null +++ b/pkg/pickle/helpers/se.leap.bitmask.policy @@ -0,0 +1,23 @@ + + + + + LEAP Encryption Access Project + http://leap.se/ + + + Runs bitmask helper to launch firewall and openvpn + Ejecuta el asistente de bitmask para lanzar el firewall y openvpn + Bitmask needs that you authenticate to start + Bitmask necesita autorizacion para comenzar + package-x-generic + + yes + yes + yes + + /usr/sbin/bitmask-root + + diff --git a/pkg/pickle/helpers/se.leap.bitmask.snap.policy b/pkg/pickle/helpers/se.leap.bitmask.snap.policy new file mode 100644 index 0000000..cac56b4 --- /dev/null +++ b/pkg/pickle/helpers/se.leap.bitmask.snap.policy @@ -0,0 +1,23 @@ + + + + + LEAP Encryption Access Project Project + http://leap.se/ + + + Runs bitmask helper to launch firewall and openvpn (${applicationName}) + Ejecuta el asistente de bitmask para lanzar el firewall y openvpn (${applicationName}) + ${applicationName} needs that you authenticate to start + ${applicationName} necesita autorizacion para comenzar + package-x-generic + + yes + yes + yes + + /snap/bin/${binaryName}.bitmask-root + + diff --git a/pkg/pickle/statik/statik.go b/pkg/pickle/statik/statik.go deleted file mode 100644 index 5cf32ff..0000000 --- a/pkg/pickle/statik/statik.go +++ /dev/null @@ -1,14 +0,0 @@ -// Code generated by statik. DO NOT EDIT. - -package statik - -import ( - "github.com/rakyll/statik/fs" -) - - -func init() { - data := "PK\x03\x04\x14\x00\x08\x00\x08\x004\x8a\x86P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00 \x00bitmask-rootUT\x05\x00\x015d\x8b^\xe4}\x7fw\xdb6\xb2\xe8\xff\xfa\x14Sz{,%\x12m'\xa9\xf7\xae\xefu\xefS\x1c9\xd5]\xc7\xf6\xcar\xbb}\xae\xab@$$\xa1\xa1\x00.\x00\xdaV\xb7\xfb\xdd\xdf\xc1\x00\xfcM\xc9r\x9aMs\xcf\xd39\x89%\x123\x18\xcc\x0c\x06\x83\x99!\xb8\xf3\xd5^\xa2\xe4\xde\x94\xf1=\xca\xef ^\xe9\x85\xe0/[;\xd0{\xd6\x83@\x84\x8c\xcf\x8f \xd1\xb3\xde\x7f\x98+\xad\x9d\xd6\x0e\x9c\x88x%\xd9|\xa1\xa1}\xd2\x81\x17\xfb\x07\xafz/\xf6\x0f\xfe\x02g\x83\xfe%6\x18/\x98\x82X\x8a\xb9$K`\nf\x92RPb\xa6\xef\x89\xa4G\xb0\x12 \x04\x84\x83\xa4!SZ\xb2i\xa2)0\x0d\x84\x87{B\xc2R\x84l\xb6j\xed\x98K \x0f\xa9\x04\xbd\xa0\xa0\xa9\\*\x103\xfc\xf1\xf6\xfc\x1a\xdeRN%\x89\xe02\x99F,\x803\x16P\xae(\x10\x05\xb1\xb9\xa2\x164\x84\xa9Ac\x00N\x0d\x05W\x8e\x028\x15 \x0f\x89f\x82w\x812\xbd\xa0\x12\xee\xa8TLpx\x99v\xe1\xf0uA\xc8\xd6\x0e\xb4\x896dK\x10\xb1\x01\xeb\x00\xe1+\x88\x88\xce!\xfd\xc6\x91\xe7\x03\x0c\x81qD\xbc\x101\x05\xbd \xda\x8c\xef\x9eE\x11L)$\x8a\xce\x92\xa8\xdb\xda\x81i\xa2\xe1\x87\xe1\xf8\xbb\x8b\xeb1\xf4\xcf\x7f\x84\x1f\xfa\xa3Q\xff|\xfc\xe3\x7f\xc2=\xd3\x0b\x91h\xa0w\xd4bb\xcb8b4\x84{\"%\xe1z\x05b\xd6\xda\x81w\x83\xd1\xc9w\xfd\xf3q\xff\xf5\xf0l8\xfe\x11\x84\x84\xd3\xe1\xf8|pu\x05\xa7\x17#\xe8\xc3e\x7f4\x1e\x9e\\\x9f\xf5Gpy=\xba\xbc\xb8\x1a\xf8\x00W\xd4\x10E[;\x9bx;C\xe9H\n!\xd5\x84E\xca\x8e\xf9G\x91\x80Z\x88$\naA\xee(H\x1aPvGC \x10\x88x\xf5\xb8\xccZ;@\"\xc1\xe78B\xd0\x05\x16\xfa\x00\xc3\x19p\xa1\xbb\xa0(\x85\xffZh\x1d\x1f\xed\xed\xdd\xdf\xdf\xfbs\x9e\xf8B\xce\xf7\"\x8bC\xed}k\xa8\xf1<\xaf\x852`\n\x08\xc4\x92\xdd\xb1\x88\xcei\x08\x0b\x1a\xc5T\x82\n$\x8b5\x0eD\x91\x19\x8dV \x13\xce\x19\x9fC@\xa5&\x8cC \x96K\xc2Ce\x14I\n\xa1\xfd\xd6P\xa7\xc3\x13\xb98?\x1d\xbe=\xc8\xfe6 \x9b\xdd\xf7\xe8\x92\xb05t\xe67\x0d\xba\x84\x85\xadV?\x8a\x80\x04F\xa8\n$\xd5\x89\xe4@\x1f\x986\xa6\x91\xc2\xbe\xd5\xa4$\x08\xa8R]\xe0\x82\xf7~\xa5R\x8006\xe5\x9e)\xea\xb7Z\xe3\x05\x85\xf7%:\xdf;\x84FIUL\x03F\xa2#c\x0c\x8cZ)\xa0\x0f4\x00\xc1\xb3\xa1\x11\x1e\x82\xa4qD\x02\xaaZF\xdf\x82DJ\xca\xb5\x99#\xa6[\xdfL\x11s\xfd\xbdc\xeb{\x88\x89$Kj\x8c\x93\x99JD)\x1av\xb1I&\x08cwZ\\h\xa3\xcd\x9a\x12IC\x08\xc5=OmT@\x145\xf3\x96p\xa0R\n a\"\xcd$\x89H\xc2\x83\x85\x8f\xd3\x8c-c!5\x08\x95~\x934\xfd\xa6\xd8\x9c\x93(\xfb%\x82\x0fTg\xbfV*\x12\xf3\xecW2u\xc3(\xdc\xcf\xbej\x92\x81iI\x02:%\xc1\x87V+X\x86\xc1\x82\x06\x1f\xe0\xb8\x00\xee\xe3\xa5\x89Ht\x9c\xe8\x96]\xa6.\xce\xaf\xc6\xfd\xf3\xf1U\xab\xd5\n\xe9\x0c\xe6TO\xb8\x98\xcc\xa5H\xe2 'K\xda\xeeX\xa57\xa31\x7fGV\xbc\x86\x01vy\xc3\xa6`\x9a\x82\x16\xc6F\xa3\xb8\x8b2\xb8\xb8\xf2\x11t\xf0@\x96qD\x95Eh>=\xb8\x9e&\\'G\xc0\x05\xe2)\xdc\xe9\xcb`a\xaeOE\xb8j\xe1\xf5#\xa9W1=\x02\xa5\xa5\x99\xd0\xe7\x82\xd3\x12i\x8e\x0bsi\xd1h\xb9\xca{\x9a\xcb\xd8\x9fS=\x97\x9c,\xdb\xbb\x16\xebn'\xbb\xed\x946\xbd\x81\xd7\xe9C@c\x0d\x7f\xa5\xab\x81\x11o\x8e\xab\x84\xb8 9\x0e\xa5\x80\xbd\xdc\x83\xbd\x9b\xdd\\\xdbM\x01\n\x87j\x05\xa4\x85\xd2\xb2\xad\x9cT\xdc}\xe5\x87\xd4\xcc\xb5\xf6.:!\xbb\x9dV\xeb\xfb\xc1\xe8jxq\x0e\xc7\xe0\x1d\x1cx\xad\xab\x93\xd1\xf0rl~\x15'\xb3\xd7:\xef\xbf\x1b\\\x0dF\xdf\x0fF\x93\xf1\xc9%\xb6\xde\xf7_\x1d\xf8\xfb\xfeA\xe9\xe6\xf5\x9b\xec\xe6\x0b{s'\x93\xb3fK\nS\xca\xf8\xbc\x0b\xf7tWRX\x10\x19Z\xb7\x08t\x10\x9b\x89\x1a\x08\xce\xa9\x9d\xd08\xf1\x94_\xc0\x0e\xc7P\xa6\xa3\xf5z8~\xd7\xbf\xfa\xeb\xe4\xe4\xbb\xfe\xf0\xbc@\xb5W\xbe39\xef\x8f'\xc6 \xd8\xdc\xe2\xf2\xe2\xaa\xd8d\x12\x1b\x1e\x8aD3>\xaf6\x1f\xbc\xeb\x0f\xcf\x8am\xd1\xd65\xb6\xaatl[\xba\xc9\xe5\xb5\xce.N\xfag\x93\xe1\xf9x0:\xed\x9f\x0cL\xc3Hx\xad\xe1\xbb\xfe\xe5\xe4\xf2b\x84\x90\x07\x7f\xf9\x8fW^\xeb\xea\xdd8\xbf\xf4b\xff\xe0\xa5g%\xad\xee\x17,X\xb4\xa7\x8c\x13\xb9\xaa\xcc\xc2S\xc6C\xe4\xbdY\x15\xcd\xbc\xb3\xad\x8ci2\xcbYm\xbe\x94\x80\x8d\xdcf\"2N#\xe3p\xe3\xe1\n\xe8u\xc1\xcb\x16\xd6\xecG\xbeBz\xb7\xb9fb\xa7\xc7 \x94o\xbe\xf9\xbf\x08\xc6\xdb\x16a\xd7\x11\x92\xeb>\x9be\xed\x98\x9a\xb1\x88\xb6\xcd\xf7N\xa3\x9a\x9b;\x96tI\x98\xa20\xc0\xa9\xc1\x04o{'\x84\xef\x9a5\x92\x87\xf0\xb5\xf2\xe0kp|\xe9v:\xad\xd6\xd0\xe8\xa6c\x97\xc7b\xaf\xd3\x1a^\x8e\xfb\xaf\xcf\x06W\xa5\xeb\x9aL#\xaa\xf0\xeea\xc3\xed\xc3\xec\xfe\xd5\x8fW'\xe3\xb3\xc2M\xb5R\x81\x8e\xbcN\xabuq98\xff\xfe\xf2|r}\x85j\xebY\x83\xe1e\xd7\xdf\x8e.\xae\x0d5\x0dv\xb4e\\\x7f\xd7\xce\x80\x16~\xe6\xf0W?^\x8d\x07\xef&\xaf\xad\xd6\xe7\x9e\x8e[\xe8<\x80\x1dxC\xa7\x8cp0\xb21\xcc\xc9`\x0d\xc2\x12d\xc1\xbf\x89(\x89{E$\x0d^RN\xc4y\x01Q\x93\xb3\xb3\x16\x91i\xdcj\x9d\x0e\xff>x39=\xeb\xbf5\x1c\xbe\xb1\xca\xd7\xeb)\xaa)\xbf3\xbaU\x1cz\x17\xbc\x03\xaf\x9b\xb6\xe1b\xcax\x98\xff\x0e\"F\xb9\xce\x7f\x87\x14\x11\xe8\x84\xe7\xd7t\xa4j\xed$]\nM{\xc6a5\xf7\x0d\x8c\xa2\xf2\x8e\xca\xbc\xc9\x92p2\xa7K\xcau\xcf\xae\xc5\xf9-\xeb\x01\xf7\x14\x0d\x12\xc9\xf4\xaaBc\xa2\x0c\x9aL\xf6\xd9\xf5\xd8lr\x94\xee}\xa0\x0d\x17Q\x16=\x16\x97\xc9v\xfb\xa2\xde\xd2\xce\xb9\x03\x7f\xdf\xeb\xb6n[-6\x83\xb2F1e<|\\\x08\xec\xd4)\xf0\xd8\xa7\x0f\x9a\xf2\xb0}\xe3\xf5z\xa8p^\xb7\x0c}\xdbi\xb5\xfagg\x17?\x14\xa4\xf2\xcf2\xa7\xbc#\xb8\xf1\x86\x97\x86\x88\xf3\xebw\xaf\x07#\xf3\xedrt1\xbe\xf0n\xcb\x8cf\xf1\x82Jl~2\xbc\xfcn0*\xdc\xdft\x8f$z\xd1|'\x97\x03\xde\x7f3\x1c\xfd\xf6\x9b\xa5\xe4\xfa|\xf8\xf7\xab\x8b\x93\xbf\x0e\xc6\xbf\xfd\xe6\xa8j\x02s\xc2\xb7r1(\xcc\xe4,\x92E\xa5E}:<\x1b\x14\xae\x1b95\\\x0eH\xd3\xd5\x99$\xf3\x8c\xc6\x1a1\x1f(\x8dI\xc4\xeeh\xf1v\xce\xcb\xbc\xe1\x1d\x95\xd3f\x14\xb5\xf1\x98f\x05\xde'\xbc\xc7\xe2\xbbCw\xf5_\xad\xd6e\x7f\xd4\x7f79\xbd\x18\xbd\xeb\x8f\x0b\x12u\x88\x8f \"\xcbiH@\x1d\x81\xa4\xfe\x92hc\xc8~\xfe)|\xfe'\xaf\x0b\xaa\xe3\x10[\x11\xafi\xdc\xd6A\xfc[\x12\xc6\xbf\xe9 ~e\xbe\xbc\xea\x94\x80\x87\x97%H\xa6&w$b\xe1\x84\x84\xa1\xa4J\xb5\xb3\x86N\xe6k\xba\xb9\xe9\xf7\xfe\xef~\xef/\xbd\xdb2i(\xc5F\x10\xbc\x8fm~\xbe!\xbd_-\xf8\xe4'\xff\xa7\xffs[\xfc\xdd\xc3+\xcf~\xfa\xd3\x7f;\xbc\xc6\\\x0d\x07\x83\x01\\\xe9\x10\x0e\xf6\xf7_\xfa\x07\xbd\x17\xfb\xfb\x07\xb6?\x14x\xb1\xbf\xca\xba\x95\x11\xf6f8Z\xd3.d\xb2\x9d\xfeRq\xc4t[un\xf6o\xb3\x11e\xfa\\\x82Wp|\x0c^\xc2\xd9Cj\x1c\xae\x87o\xd62+\x1b`\xca.\xa3\x0bfQ\xd0T.\x19\xa7\xf0\xb7dJ\x15\\\\\xa5A\x97\xaeY\x83 _\xb5\xfev\xfdzp5\xb9\x1c]\xfc\xfd\xc7\xc2\xeaM\x1f\x98\xd2\xaa\xed\xed\xdd\x11\xb9'\x13\xbe\xf7\x0f\x03\xbf\xa7\x17L\xf5\x98\xea\xc5R<\xac\xee\x96^\xa7UZ\xc9\xcdH\xbd=\xaa\x03\xdb\xdcs\x0b\xba\xed\xe2\xe4\xf4-.\x1d\xf2~/\x10|\xc6\xe6{^\xe1\xee\xf0\xf2\xbb\x8b\x8b\xbf\xc2q\xa1\xf1s\xf0\x10M\x8f\xc5\xbd`A\xf8\x9c\xf6\x16B|(B\x9d\xfe0\xc9|\xd8\x06\xc0t\xd3\x86F\xc0\xd9o\xb7+\x98\x95v@$\x8a\xda7\xa9{\xd053+bJ\x9b\xb9\xfa\xb7\xd7W\xbd\xd3\x8b\xd1\x0f\xfd\xd1\x1b\xef\xb6cd\xb2\x9f{)\xb6K\xeb\xab\xbe\xc2\xab4R\xb4\xf9\xfe\xcbV~o\x07\xcd6qBQ+\xa5\xe9\xb2U\x05\xd8o\xb5Zo\x06\xaf\xaf\xdfZ\xb1\xccq\x9dl{x\xc9\xeb\xb4\xc6\x03t_\x0bw\xcc\x15\xe3\x8d\xb0\x19`#\xdb\x97\xdb\xffDb>g|\x8e\x97fB.\x896\x9b\xdc\xe3\xf4\xba\x7f\x9a^+\xcc\xa3\xaf\xdbD\x05\xc6\x8f\xef(\xe8\xc1\xd7m\xe3\xb1\xb8\xaf\x11\xbd\xa3Q\xe1\xf7\x92*E\xe6\xb4c\x1c%\x03\x1b,\n\xb8\xaf\xb4\xa4d\xf9\x1d1\xee\x84l\xa7\x0d|E\xf5\x99A\xd3N\xdb!\xd5\xc5\xdb9Q\x19\xc9\xf6\xb6\x81(\x91?\xa7\xfa\x0c\xaf\xb5'\xe8XM&\xc5\x86\x1b\xbbrMH\x18\xa6\x14\x06\x8bN\xabew\xdb\xe8\xd4Db\xde\xb6j\xd6\xb1\xfb\xe3\xeb1\x06\x08[;n\xfbU\xb3qw$Jh\xc5;\xff\xde\xb4 \xdaE0\xad\x9b\xae\x14\x0d\x81\xc56\xe6\x86(`x \x0e\x8b\xef\x1cv\xdc\x1a\x01\xa2L \x8b\xac\xd1\xb0\xa1\x7f\x1f\xdazA\xab\xac\x0c\xe9\x8c\x18\x83\xc3\x14h\x99PHxd46\xa4\xda\xe8\x9b\x90\xc0x\x9cd\xb7Sf\xbe\xa7\x0fL\x07\"\xa4\xef\x8d!\xfc@S\xd2\xba\x98$,$#0\x17\xc1\xb8\xd2\x94\x84F\x9b\x0dQ\x99\x9a\x02\xbc\xb7\xfd\x14\x87$\x13\x8c\xf5\xdb\x1b4\xccr )\x04\xd2S\x040\xfa\x93\xcd\x02\xa3\x87\xf7\x92i3C\x14\xbaE\xa0E\xd7Q\x84\x91\xd1\x05\x85K\x03Re\x84\x98\xfeB\x03\x9du\xa3\x17R\xdc\x97\xe8\xc2\xe0\x18\n\xcb\xad\xc8\x86\xb9zA%E\xf7\"\xcdG\xb8\xc1\xd6\xd0W\xc6\x9ej@L\xa4\xc6\xb9\xe7Fp\x9b_M#\x0cF\x85,\xe3C:Mp\xed7\\2K=\x94\x96z\x0f<;_\x10\xdc\xa8>BM\xd2\x9c\x84\xd3A\xe3\xcc\xb5=\xbc\xe8uqt\x16\xfb\xc4 \xbd\xd2\xd0^\xf5\xba\xd6epM\xadVTZ\xe2\xc5J\xc3L *m\xd3\xeb\x95\xe6\xc8\xf6j[\xbc\x987\xc4\x96n5l\xbb\xc1 \xe9`q\xd5\x98\xe4\xeak)-\xad\x84\xd5KP\x08\xe9\xe7{\x16T\x12\xcb\xc9.\x18#\xc8\x8f\x8bw\x87\x97\x83\x82e\xa9-6\xcd\x88\x1a\x93\x13YF\xa5\x8c\xa5f8Cz\xc7\x93(B\xe6P\xde\xde\xdd\x0b\xe9\xdd\x9e\xb9\xb2\xdb\x85\xdd\xfbJ\xe2\xa3\x96~\xc2-X>\x1a\x91\xe8c\x87\x10\x7fS)\xd3\xdf\x8dD\xee\xe7c\xb5V\xb4\xd0\xc1 \x06\"/\xed/\xb4\xad@\x94iWY\x7fg\xb96\xd4Wp63\x10\xbe\xed\x0e\x15\xe6\xabc8h^\xe9w`\xdf\x88\xf6\x00\xad\x12\xba\x80\xd4\xa5\xb4\xad\xfd!|\xa5\x17f\xb2\x1b\x8e\xae\xc1\xe0\xf2\xe6Sj\xf7'\xa1\xdf\xd8\xceM8\xe7h\x9f \x88Q;c\xa4\x8c\x07\x8eQ\xf1FH\xfbi\x9b1\x05\xcb\xb0\x8b\x83\xb3\x96\xb9\xd3\xcdM\xc81}\x08\xea\x1eRf?\x8b\xfc(\xb5\xa2\x91\xe1&*|\x833\x86\x96\x8a>\x04\x15\x90\xaa\x8e\x9a\x8f\xb1I\x8f\x8do\xcd(\x9a\x87]\x19\x9a]\x12\xcd\x8ee\xa9\xe6\xc7v\xad\xcc\x9b\xd8\xdf\xb1dB2\xbd:v[\xc2\xb3\x8b\xb7\x93\xe1\xf9\xe9Ee\xad\x8c%\xe3X\x89c\xd0\xc1=\xb1\xf6\xdcm\x8c\x8d>dx\xcb\x1b\xbb\xa5\x9a\x1f9{B\"g\xa4\x1dT\xd1\xcf\xc7f\xd9\x96\xae\x114\xc7_\x01\xcb6i5`;$\xc0\xed|\x15Jp\xccd\x9b\xe1\x0e\xde\x0dFo\xbb\xf8\xb5\x7f6\x18\x8d\xed\xd7\x93\xd1\xd0}\x1b\x8cF5~\x9b\xeb?\xf4G\xe7\xc3s\x07y~1\x1e\x9e\x0c\xecw\xc3?\xfb\x0dw\xe2%F\xb2\x99\xe9\xbf\x1e\xdc\xce\x98lw\x97N\xf6\xe9\xe2\xb2T\xf3\x82/\xe5De\xff\xb4S \xdaVi'\x85\x85\xb2\xa9+6\x03\x8c\xb1\x08Y\x0c\xa7\xa4\x9f,\xab\xee#I\x13\xfa\x10\xb4\x1f\xef>\x87\xb2\x01\x0d\x0b\x96\xaa\xa1\x9d\xcd\xeb\x14\xb1\xa2n\xff\x93(m}\x9bH\xcc\xad]a\n\xd4\x07\x16\xc74L\xdd$$\xdc\x87kE\xa1\xa2\xbc\x88\x03\xeb\xa1\x8cK\x82=\xa7Z\xa7\xa0}O!\x14|W\xc3=\xe1\xb8\x8d]2\xa5\x8c3\xb1\xec\xf8UY\xada\x92\x9bS\x85Q\xa4\xc3\xc49\xbd\xe5(\xc9\x94\x1bFE\xe8\xac\xf9\xd9x\xdb\x1d;bsU\xd9\xe2';q\x94&:QXbR&\xb4\x89\x1cd\xc1\x03\xd3\xed\x03\x17\xd7q\xf9\x91,\xaecvQ.\xab5\x992\xbe\xa1\xea\x01\x93\x9f\x86\x93\xae(\xce\\\xb3;\xa7\xac\x1a\xc5\xe5\xc5\xcd\xacJ\xfd\\W\x87\xb4\xc0\xca;m\x9d\xb5\x1a\x7f\x85\xf2)\xbfcRp\xf46v\xaf\xce\xfb\x97\xbb\x1d44\x95Xt5SW\xd81\xdaZ>\xc5I\xec\xecEa\x81\x91t)\xeehhk\xa1L\xb3\xd0\xa6\x12c\x12|\xc8\x8c\x10\xe4v\xbf\xdaM\xab@i#9Y\xf6\xb2S\x8b\x0b\xd4\xdbXl\x96\xde\xb4N\xabD1\x89\x94\xd8@\xb6\xff\x085i>t=-i\x0b\xa7\x041\x91\x8afj0\x8b\xc8\\Y\x97\xb7\xac\x0cc\xf2\x81\x02\x91\xf3dI\xb9\xb6\xbb\xe3\x8c\xb2\xd4\xfb7\x1b@\x14\x1c\xe2\x04\xa6\xbb\xb60\x8eD\x91\xb8\xc7-\x81XZ\xcd\xb0\x11\xf2Db.\x17\xb0\xd3t\xd5@\x03m\x08h\x88\xf1T\xa33%'\x0d3\xfc\x11\x99c=@\x1e\x04\xb3;@\xef\xe7^\xcf\xab\x84\x18Lc\x8c\xa6\xc21~\xbf\xd9\xbf-\xddg\xb3B\x13\xc6\xa1\x94M\\\x17\x84I7\xd7\x19d\x93\x7f\xf1\x8f\x84I\x1aNlQ\x08\x1c\x971\xdfd\xa0\xb75P\x0c$\x95\xa0\xd7\xb9h\xb3\x88hMq\x93\xa4\xa8i\xaa]\xb2:\xc6r\xb1& \xec7\xa3\xe9\x86\xb9\x12\xb6)\n\x9bqX\x92\xb89\x14`>\x85\xcc\x8e\xcb\x02u\xba\x96\xab\x07G\xb7\x1dD\xc5P2\x16_}h`\x87\x17Q\xde.\x10\xd21~\xa8\xb9V\x19\xf6\x9a \x144\xc4g\xcd\xd2G\xb9H\xe6\x0bW\x85\x83\xc4<\xe6:B\xbe\xf4\xe6\xa2\xac\xcb2\xfdT7\x13\xd5\x0f\x06hL\xef]\xfbg\x82j\xce8\xfc\xca\xe2\xe2x\xbbU\xf9n\x18(\x96\"Y,9N\xc7|\xef\xb7\xdf\xaa\xea^\xfd\xb0\x19\x94\xb2\xa97:\xa6\xb7m\xc4\xf4\x08$\xd4\xb4\xddB=\nT\x8e\x11V?\xcd.r\xf1S\x15\xedk\x12\xe66\xe9q\x89BQ\xaa\x96\xe4\xcd4\xaf\x93j3\xa5H\x9ds \x8f \xe1\x92\x06b\xce\xd9\xaf4\xcc\x16I\xb4O\xd6\xb3k0\x10\xf5`r5\x0bQ\xdb\xd9\x959b\x04z5<\x7f\x0b\xd6DU}H\xb3%\xc8{+\xe7\xd1\n\x08\xe7T\xfa\x99\x1b\xd1.m\x91\xea\xa5y\xe9\xc2\x81\x05\xa6M\x0b\xc7\x19V\x86\xa6\xed\xba\xa0\x08g\x9a\xfdj\x96\x03\x0c\x04t\x0bu\xaci\x84\xa8R\xc8\x8a\x1e\x10\"C\x0f\xc3\xf12\x8bL\xe1\x1d\xe7\xf5\xdb\x85#\xd5\x89t\x7f\xea2QZ\xa4\xc0\x16d\xd3RSZ\x10\xe1x\xfd2\xd9r\xbc,\xdd\xcay\x9a\xd7>\xd5\xbc\xad\xacM\xda\xc7\x8dk|\x0b\xcf\x8b\xa5.\xf0\xbc\x8c\xfcq Z\x95H\x0b\xd8S\x86\xa1\x1fi\xc9+\xaaEe\x0e\x18pl\x94_\xc6|.=\x82\x19\x93J\xe7\x13N\x8bl\xe5g\n\xd8\x9c\x0b\x99\x86\x00\x82Di\xb1$\x92E+P\xd44- +\xb8\x0c\xb9\xf3e\xdc\xc0\x07\x1a\xdc\xa5\x8e\x8c5\xbb\x8e\x88\xf2|C\x07{\xd7\x99\x80 \xdb4[\xa7#sGm\x04k\xb7S\xd3S\x117\xa9\xe9\x95\x16qI\xbd0\x8di\\W\xe3\x94cu?\xe5\xae\xcc\xd9\xd5\x8f\xd9\x12\xc8G\xf5ok\x95\x8bq\xa1=nHn\xe0m\x17\xe3\xef\xda\xa8\xb91\xfa\xe5T\x14\x9b\xc1t\xa5\xa9j{i\xa5Z\x17\xd2\xd2Xln\xc0\x8c\xb0l\xabB9Z\xbd]Y\xa3\x84\xf2?\xb0(j\x9b\xbdi\xcc\xc2N\xd7\xd5q\xfbW\xc3\xb7\xe3\xc1\xe8]Y\x81\xac\x91\xc7\x0d\xc7\xe9p4\xf8\xa1\x7fvV\xdaq\xcc\x89\xa6\xf7d\xa5\xda\xe9\x97Z\x99'&\x02I:k\x15\xfdGBy\x80\x9b\xf5\x14\xa4\x18RF\x17\xd3\xe5\x8dYh6 \x15\x9b\x90\x02\x1d\x01iD\x96g\x1f\x8b\x91\x85\x1c\x8ai*\xc94\xb2K@\x9a\xdb)]\xac\xf9\xa7.9QM\x9cw3\xac\x99\xd9\xc0p\x0f\x82U\x14<\x8d\n\x9d\x0b7\xb0\x8c\\|2`\xc6h\xe85\xcd\x8eu\xe92\x97i\x98\x84\xf4\x8e\x05M\x15\xee\x92\xd1;Z2\xbfir\x82S}/\xe4\x07\xb0\xa0\xfe\xe6B[)\x12\x8d\xc9\xad5%\xf8\xed\x9b\xe1e\x17\x1e\xcbb\x19\x15\x9b\x15\x81\xbf*\x95\x13\x9aO\x8e\xac*|\xa7Nv\x8cVrO\x1d\xa3\x83\xfa_2F\xab#O\x1d\xa3\x83\xfa\xbcc<~\xfa\x18Kfa}cW`\xe8\xe6x{\x0b\x93\x90\xb6}\x85\x86+\xfduX\x99)\xf1\xabu\x18\xdd\xfd\xc3\xb5\xf7S\x92\xd6b\xd8D\x13\xee\xb1\x90s\xaaL\xd1\x1aC\x98\x17\xe5\xae'd-\xa9\xeb 9\xfc\x08B\x0e\x1f\xa3D\x8b\xf9<\xa2v\x11\xb5\x89\x88\xe3\xdd\x90)\x83i\xd7\xd1\xc2fi\x8a\xe2\xf8\x18\xb2\x9b\xb9&\x18-M\xab\xc0p\x1e\x14ZS\xde\xd8x\xbfI\xa1p\x9d\xc1\x9f\xf9Bj\x1f`\xea\xc2n\xef~\xb7\x0b\xbb\x9cj\xdf\x90\xea\x07\x82\xcf|\x12E\xbe#\x07\x07p\xfc\xb5\xda\x85\xafM\x17\xb7fxf\xb3r~1\x1e`y\xa2\xa2\xa0\xa9\xb2\x9b7\xb7\xc0\x10\x08\x16\x84qk\xb9\x15,\xf1!P\xc6\x03!%\x0dt\xb4\xca\x8a\x92H\xa4\xa8\x9f>\xee\xbf@\x83\xa4\xe0~a\x9f\xcb\xaf\x95\xaa\xe0\x83\xbdX\x91\x96\xadr\xb64;\xdd\\\xbe\xb7\x0f\xd2\x9fbr\x85)\x90\x94(\xc1\xbb\x16\xa3\xd9\xca\xc6i\xc8$\xad\x05\xef\x02&\xb7p\x81\xd22Q\xe8\xd6\xb4v\\Y\x92\xd9\xf6\xb0\xf8\xeea\x82\xc3\x99\xb82\xf8\x8eM\xaf\xb92!\x12\xe1.\x83hM\x971\xee\xf2\xad\xcdq\x07& \xa4\x9f\xd7\x06\xc7w\xaf\xca\xd8\xf0G\x17p\xf0\x99\x7f/\x99\xa6\xed\xdd\x83\x9fx\x16NL;\\\x1f\xf7~\x8d\xe7\"$q\x89\xbe\xa6`aZi\x9c\xc7\xb6\\]\xbd\x0d\x94\xa7\xb5Q\x1bC\x87\xe5\xe0\x8e\x8b!V#>-+\xc6jL\xc2\xb5n\x0cV\x14\x11\xac\x81?\\\x07\x7f\xd8\x08\x9fE\xb1\x8e\xcb\xc1@\x1bK\xc7&;f\xdb\xeaB\xc8n\x8ee\x0f\xbac\xacN\n\x8e\x1c\xbb\xb8\x1e_^\x8f]\x13\xa3\nS\xe1\xf2\x04v\xa9\xda\xb5a\xb8]\\\x02\xf0\x02'z\xd7\xdaV\x95%\xd2\x8d\x1a\xd7'}\xe9\xd9\xf7\xc2\x8e8\xf7\x0b\xcdl\xa4\xf7=\xab\xe6](\x03<\x01y\xfa<\x7f\xd7\x92\xb7\xae/\xfc\x86O\xe0\x12\xb7\x8d[\xdb{\x8a\xf1\xc9T\\^\\}j2\x0c\xca*\x1d\x87\xdb\xb2\xfap{Vo\xa61\xdf\xfdZ\x9d\xf1\xaaK\xab\xd7\xeb\xfd\x92,\xe3G8\xb9m'f\xd0\xa3\x8b\xeb\xf1\xf0\xfc\xed\x93z*0+\xce{\xaa\x13\xbf\x0eI6\x810*\n\xc6 \xe2\x0c\x7fs~\x05\xe2\x8eJ\xf8\xfe\xf2\xdc\xb5h\xdb\x94\xcey\x7f\x9c\x0740\xc8\x8b\xc6\x12\xe7u\xc25\x8b\xe0\x03\x95\x9cF\xf0\xd2\xff\xb3s'\x1amv*\xe1\xe2\xe3\x96f\xd6\xe5O\xfc}{\x0c/s\xd9\xee\x80\xa4hV\x91\xb6\x98\x04\x1f\xa8\xb6\xb1\xb6\xef/\xcf\xcd\xb5\xfftO\x0f\xc6\x92\xa6\xc5\"T\xc1\x92\xa8\x7f$T\x92B=fQ(\xba$\x8fY\x94\xa8\x85}\xa4\xbb\xf7\xb7\xd7W\x85\xa5h=L\x1eBp@\xe6\xaa\xe1\xb3\x97\x84qM\x96`\xe5\x19\xc6\xc2\xca\xe7\x9b\x97%\xd9xo\xce\xfbNZZ4\x02\x17\x8e\x03y\x0e\xde\xd17/\x7f'\x91:\xf8\x8cD\x96\xfd\x83\x1d[\xe6\x03!\xc7\x8d\x13.\x07\x0b\xe1\xd6)\xd8n<%e\xc6[\xb1\x14Z\x04\"zL\x02\xd4>\xcez\xf0\xe2\xcf\xfe\xbe\x7f\xe0\x1ft\xed\xb7\xfd\xc2\xb7t\xdc%F4\xa3KY\xd3?9\x19\\\x8e\x0b2\xc9\xd5\xd6\xcc-\x91\xe8\xb9\xc0\x87\x81\x9c\xfe\xba\xb3\x80\x9c\n\x83=|\xa1\x00\xdc6Wq\xba)\xb1\xa4\x9a-\xa9B\x80\xf1\xc9\xe5WO\x12\xfc\x9a\x05\xe4\x0bV\xd5\xcd\x14\x7fV\xbd\x05\x94\x85\xb5c\xb9E\xc1\x93|\x94\xb0y\xdf\xa2Q\xb2\x12\xd7\x94\xc3t\x05\xa6S{FV\x8e \xcf\xb6\xb3Im\x0c\x07\x80\x12\x89\x0c(\x0c/\x95\x0f\xfd8\x8eV\x05\xbb\xe5\xd2\x95\x02\x01r\n\xbb\x05|\xb5\xeaT\x828\x98N\xc1\xb2\xb9\x85\x93MR%\xa2\xbb4y \x1f+\x0e\\\xfb7\xce\xae&jK\xedj\x93u\xa3\xfc\xde\xf5\xaf\xfev=\x18\xf5\xdf\x0c~\xaf&}b\xd2Q\x19\xb7'\x1d\x11\xa6\xd6\xcf\xe6\x8f\xd2\x84\xa9\x96d6cA\xba8\xd6]\xec\xba\xf9,#0\xe43\x8eU\x92\xaa\x91G\xebl\xe7zn8t^\xb7\x81\x9c\xae\xcbD\x95\x1d\xf7\xa7\x1b\xc9\xa6\xa1\xd8Ia\xd7\xf87\xe7WE\xab\x98\x96\xbc\xa1\xae7\x80\x9a\xd9\x88\xd3r^\x9b\x8f\xe7\xfdq\x01\x93A0\xa7:\xab\xf7YH,\xfbKgu.6\xbd@R3\xe2\x98hS\xf0\xdbM\x85,\xff\xfa\x88\xd1\xa8\xc1\x15\xbbz\x1c\xfc\xd3J\x00i\xc6%\xc7R\x91\xc9\x02\xf7>\x99@\xd6\xf0\xbf\x91\xe1fc.\xc5jk\x96\xe3\x1c\xf8\x08~\x97\x95\xbe\x91\xf9\xebm\xcafde\x894L\x9c-{\xf9\x92d\xb5\xcd\\\xf9t\xa2\xfbX\xdb\xb5^\x8c\xcd\xf6h3\xb6\x8f\x90\xe3\xe70{_\x9e 7\x9a=h\x1bO\xbb\xd3$\xc6\xc3\x8f\xb4~\x87_\xbe\xf5;|T\x10\xbf\x9f\xf3\xa5\x8d\x8c{\x1e\xa0r\xe0\xa0\xc0\xc3\x06k\xc5l\xeeb\xb9\xb2\xac\xf8 [\xf5\xdd*\xd9`l\xf9\xb8\xd2\xee}{\x11\x9diHb\x7fC\xe8\xc1\xbb\x8c(Q\xd4\xc8\x1e\xde\x97^o\xf6\xde\xd6\x00\xfb\xd9S\xa0\xb3{\xf7\x86\x9c\xed\xcaD\xed;\xc66\x15\x8b\x9aV\x89\xa2\x12\x12\x16\xa6\xef\xcfK\x85\x94\xbe/\x0b\x1e\xab\x16\xddP`\xe9^\xfdS*\xb3\xb4\xbbulW\x14\xc5c\x05\x85\xf8\xbe\xa0'WN:\xb0JG\x9b*\x06\x1b:\xda\xa6n\xb0\xd4Qc\xb5\x1d\x8e{\xfd\xf6'E\xb0\x1dK\xd3w$\xad-`}2k\xb1.\xf1\xe3\xd8\xdbT\x1c\xfa8\x8bk\x1dn\xcf\xe6b\x0de\x13\xab\x1f\xdbj\x16\xb18~\xbf\xb1q\x0e\x9c\x01\x04_\xb3gX\xcb\x96$F\x03\xa6\x96:\xb6~\xb3H\xb4b!\xad\xf5\xbe\xa6\xa0\x04{\xaa\xac\x8dHk\x8fqM\xe5\x8c`\x82\xbc\xf2\xa6\xabn5 \xd5P\xd0TH1e/\xc4Z\x9f\x0f\xffB(\xcd\xde\xd3\xf5\xef\xa0tc\xd1M3\x8f*\xd9\x8e\x7fK\xcf\xcdc.\xa7 \xdc\xbc\xc1\x8741y\xa4+\x87)\xe1\xebLn\xd1\xe2\xdf\xec\xdfnZ\x89\xce\x05\x1as|\x16\xca\x0d\xcc\xfc>\x06\x07\x9b\xe7<\xdc9\x17 g\x0f\xb0\x9b\xb0p\xd7\x96\x9e\xe5\xda\x9f\xaf!\xe5y`\x06\xa6\x9e\xc42\xf4\xdfkl\x13\x89\xde\xa8[5\x00{\xfa~\x17\xe6\xd3t\x9aC\x85\xa3M\xe70\xd7\x11\xa5\xe1\xd4L\xfc\xf5&P\xee\xcf\xec.kmJ\xe2\xc8O\xb8\xb5\n\x95\x02\xe3\xaa\xb3%\x1fj\x0c}\x8c\x0d5\xb2\xb6\xe5\xc0Z\xd2E\xfc\x11\x94\x1b\xc3W\xa1\x9c5\x9d9Tr.:u\xaav@\x0baV\x88\xa9P\x14\x14\xe3A!m\"\xa2HayZ\x0d,&\xaa|q\xd3[|\xac\xefQ\xab\xdd`\nBq\xcf\xbd\x8d\xe3u\x86\xfe \x1a\xdb\x90X\xfa(ym\xab\xb1\xe5x\xc8\xf6b7\x1a[\xf6W\x1e\x97~\xce\x8d-\xf5v\x03q\xdb\xf2a\x93\xde~,\xfd\x1f\xa5\xbd\xb5tV\xfa\xc9\xce\xf5.+Z\xc5\x19d\n\x92x\xc3\xf1\xde\xdbkp\x1dq]\x8f\xab\xb8\xaa'\x17\xab\x04\xdf\x9ai\xcf\xfa\x86\xb1\\\x95}F\xb3H7\x1e&\xf0qx\xf0\xdd\xd0\xe9\x0b\x92Q\x14\x93\x89q\x82&\x13'\x05\xb7K\x80\x8a\x0b\xe2Xd\xfd%K\x8d[A\xff_\x00\x00\x00\xff\xffPK\x07\x08/\x1c\xa5\x87\xb7#\x00\x00\x04\x8f\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xb4\x037P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00se.leap.bitmask.policyUT\x05\x00\x01\xf4\xe8(^|\x93\xcbn\xdb<\x10\x85\xf7z\x8a\xf9\xb9\xb7\x98\xec~\x04\xb4\x82\\\\\xa0h\xd0\x1aE\xb2\xe8*\x98\x90c\x991=\x14\xc8\x91m\xe5\xe9\x0b\xc979\x01\xba\xb3\xc93\xdf9\x9c\x19\x99\xdb\xdd:\xc0\x86R\xf6\x91\xa7\xea\xba\xbcR@l\xa3\xf3\\O\xd5\xcb\xf3\xb7\xc9\xff\xea\xb6*\xcc\x7f\x8f\xbf\x1e\x9e\xff\xccg\xd0\xc4\xe0mg#/|\x0d\xf3\x97\xfb\xa7\xef\x0f\x05\xa8\x89\xd6\x8bD\xe4(\xaf$6Z?>?\xc2|P\xfe\xf0r\xf8\x05\x0fCQ\x9bP|d\xb8.\xaf\xb4\x9e\xfdT\x05\xa8\xa5Hs\xa3\xf5v\xbb-G\x942\xa6ZgAv\x98\\\xd6'\x9c\xbe\xd6\xe3\x10\xa5\x13\xa7\xaa\xc2\x8c\xcf\xaa\xa2\x000\x1bb\x17S\xf54\xbb\x9b\xc3\x8cm\xea\x9a\xc1\xf8\xceZ\xca\x19\xe6)\xbe\x93\x15\xa3\x0f\xb2s\xc5k\x9bBu\xc8\x14\x08\x9b2\x93>\xca\x86\xbb\x81\x8ev\xa0y7U\x99\xcaA\xf7\xe6e\x8dyU\xee\xa3\xa8\x1e `\x1ce\x9b\xfc\xe0]\xfdn9\xc3A\x06K\n\x0d%\x90\x08\x01[\xb6KX\xf8D[\x0c\x01\x90\x1d\xc4\x86x\xd3\xb0\xd1\xe3\xfa/D\xd8\xad\xc3M\xc0~X\x94U5{'\xdb\n\x02\x05\xc0\xec\xb3\x10\x0b\x81\xa3\x93e\x83 ! \x7f`\xea5'\xc3\xee_vk\xca\x19k\xaa\xee\x0f\x10&r\x19d\x89\x02]l\x01[Y\x12\x8b\xb7(\xd4?&\x0b&1\xfaXu\xc1\xf8\x14\xf7L\xb4\x94\xbd`\xcf\x8a\xc9\x7f\xa0\xed\x9f6\x84\xb5qM}\xdc\xcf@o#\xbf2\xae\xa9j\xd0\xae\xb0\xa6\xc9nR\x13S\xf2\xd6\xe8\xf3%\x1c;\xb6\xc06H\xde\x17\xf7\xd3\x0b!n_\x91\xbb\xaa\xa3l\xf4\xf9\xef\xa5\xc0s?\xe6\x0d\x8dU\xa7\xb3O\xac/\xc2\xb1\xac\xef\xeb8\x82A\xe6(}\xc7V\xd4MUL\xf5\xc5\xea\xef7h\xe5\xa5\xa4\x1d\xd9\xb2AY\xaaJ\xb79\xe9\xfc\xe6Y\x1f\xc69I1\x8a\xd1G\xd4\xb0\xc2z\xbf\x97Ua\xf4\xe5\x17\xf17\x00\x00\xff\xffPK\x07\x08\xdf\x04|\xcb\xf7\x01\x00\x00\xea\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xb4\x037P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00se.leap.bitmask.snap.policyUT\x05\x00\x01\xf4\xe8(^\x94\x93Oo\xdb<\x0c\xc6\xef\xfe\x14|\x85\x1e\xde\x1db\xb5\xb7\xa1P\\tm\x06\x0c+\xba`h\x0f;\x15\xac\xcc\xd8jdJ\x90\xe4$n\xb1\xef>\xd8\xf9S\xa7\xd9e'\xdb\xd2\xc3\x1f\x1f\xd2\xa4\xba\xda4\x16V\x14\xa2q<\x15\x17\xf9\xb9\x00b\xedJ\xc3\xd5T<>|\x9d|\x16WE\xa6\xfe\xbb\xfdq\xf3\xf0k>\x03\xef\xac\xd1\x9dv\xbc0\x15\xcc\x1f\xbf\xdc}\xbb\xc9@L\xa4\\\x04\xa2\x92\xe229/\xe5\xed\xc3-\xcc\x07\xe5w\x93vop3\x04\xb5\x01\x93q\x0c\x17\xf9\xb9\x94\xb3{\x91\x81\xa8S\xf2\x97R\xae\xd7\xeb|D\xc9]\xa8dL\xc8%\x862\xca\x03N^\xc8\xb1\x89\xbcL\xa5(25>+\xb2\x0c@\xad\x88K\x17\x8a\xbb\xd9\xf5\x1cf\xacC\xe7\x87\xc4\xd7ZS\x8c0\x0f\xee\x85t\xda?\x95\xdc\xc9\xdf#\x9f\xda`\x8b\x9d7K\xe8\xf3Hr/\x1b\xee\x86,\xa8\x07\xaa)\xa7\"R>\xe8\x9eMj0.\xf3\xb37\xf4\xde\x1a=\x14|\x8f\x0d\xfd\xce\xb7.E\x9f\x05@\x95\x14u0\x83\xad\xe2g\xcb\x11v\x91P\x93\xf5\x14 9\xb0\xd8\xb2\xaeaa\x02\xad\xd1Z@.\xc1y\xe2\x95g\xf8\xff4\xc1'%\xc7\xd0\x934\xb0i\xec\xa5\xc5\xfe\xe7R\x14\xc5\xec\x85t\x9b\x10\xc8\x02F\x13\x13q\"(\xe9\xe0\xc3c@\xb0\xc8\xaf\x18z\xcd\xc1E\xf7\xcf\x1e\x1a\x8a\x11+*N\xe5\xc0De\x84Tc\x82\xce\xb5\x80m\xaa\x89S/\xa1\xbe\x031aHJ\xee\x01G\xb8\x0f\xe5\xfc\x15\xae)\x9a\x84=\xd6\x05\xf3\x8a\xba\xef\xc2P\x97v\x0d\xf5\x95}d\x1b\xed\xf8\x89\xb1\xa1\xc2\xa3^bE\x93\xcd\xa4\"\xa6`\xb4\x92\xef\x97\xb0o\xee\x02[\x9b\xe26\xb8\x1f\x08k\xdd\xfa \xb9+:\x8aJ\xbe\x7f\x1e\x0b\x0c\xf7\x93\xb3\xa2\xb1\xeap\xf6\x81u\"\x1c\xcb\xfan\x8f-(dv\xa9o\xde\x92\xba\xa9p\xa1:\xda\xaa\xed\x04.M\xcaiC:\xf7\x98jQ\xc8\xc8\xe8\xe5\xb3ay\xf6\xf6l\x18C\xb7\x1d\xd7\xdd\x1cL\x82sI\xc9=x\xd8\x11\xb9\x1d\xfc\"S\xf2x\xf5\xfe\x04\x00\x00\xff\xffPK\x07\x08\x01\xb4\xa1\xf7\x1a\x02\x00\x00S\x04\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x004\x8a\x86P/\x1c\xa5\x87\xb7#\x00\x00\x04\x8f\x00\x00\x0c\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00bitmask-rootUT\x05\x00\x015d\x8b^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xb4\x037P\xdf\x04|\xcb\xf7\x01\x00\x00\xea\x03\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfa#\x00\x00se.leap.bitmask.policyUT\x05\x00\x01\xf4\xe8(^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xb4\x037P\x01\xb4\xa1\xf7\x1a\x02\x00\x00S\x04\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81>&\x00\x00se.leap.bitmask.snap.policyUT\x05\x00\x01\xf4\xe8(^PK\x05\x06\x00\x00\x00\x00\x03\x00\x03\x00\xe2\x00\x00\x00\xaa(\x00\x00\x00\x00" - fs.Register(data) - } - \ No newline at end of file -- cgit v1.2.3