Move Linux client & common packages into a public repo.

This commit is contained in:
Earl Lee
2020-02-05 14:16:58 -08:00
parent c955043dfe
commit a8d8b8719a
156 changed files with 17113 additions and 0 deletions

14
cmd/relaynode/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
/*.tar.gz
/*.deb
/*.rpm
/*.spec
/pkgver
debian/changelog
debian/debhelper-build-stamp
debian/files
debian/*.log
debian/*.substvars
debian/*.debhelper
debian/tailscale-relay
/tailscale-relay/
/tailscale-relay-*

63
cmd/relaynode/acl.json Normal file
View File

@ -0,0 +1,63 @@
{
// Declare static groups of users beyond those in the identity service
"Groups": {
"group:eng": ["u1@example.com", "u2@example.com"]
},
// Declare convenient hostname aliases to use in place of IP addresses
"Hosts": {
"h222": "100.2.2.2"
},
// Access control list
"ACLs": [
{
"Action": "accept",
// Match any of several users
"Users": ["a@example.com", "b@example.com"],
// Match any port on h222, and port 22 of 10.1.2.3
"Ports": ["h222:*", "10.1.2.3:22"]
},
{
"Action": "accept",
// Match any user at all
"Users": ["*"],
// Match port 80 on one machine, ports 53 and 5353 on a second one,
// and ports 8000 through 8080 (a port range) on a third one.
"Ports": ["h222:80", "10.8.8.8:53,5353", "10.2.3.4:8000-8080"]
},
{
"Action": "accept",
// Match all users in the "Admin" role (network administrators)
"Users": ["role:Admin", "group:eng"],
// Allow access to port 22 on all servers
"Ports": ["*:22"]
},
{
"Action": "accept",
"Users": ["role:User"],
// Match only windows and linux workstations (not implemented yet)
"OS": ["windows", "linux"],
// Only desktop machines are allowed to access this server
"Ports": ["10.1.1.1:443"]
},
{
"Action": "accept",
"Users": ["*"],
// Match machines which have never been authorized, or which expired.
// (not implemented yet)
"MachineAuth": ["unauthorized", "expired"],
// Logged-in users on unauthorized machines can access the email server.
// Open the TLS ports for SMTP, IMAP, and HTTP.
"Ports": ["10.1.2.3:465", "10.1.2.3:993", "10.1.2.3:443"]
},
// Match absolutely everything. Comment out this section if you want
// the above ACLs to apply.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
// Leave this line here so that every rule can end in a comma.
// It has no effect since it has no matching rules.
{"Action": "accept"}
]
}

1
cmd/relaynode/clean.do Normal file
View File

@ -0,0 +1 @@
rm -f debian/changelog *~ debian/*~

13
cmd/relaynode/clean.od Normal file
View File

@ -0,0 +1,13 @@
exec >&2
read -r package <package
rm -f *~ .*~ \
debian/*~ debian/changelog debian/debhelper-build-stamp \
debian/*.log debian/files debian/*.substvars debian/*.debhelper \
*.tar.gz *.deb *.rpm *.spec pkgver relaynode *.exe
[ -n "$package" ] && rm -rf "debian/$package"
for d in */.stamp; do
if [ -e "$d" ]; then
dir=$(dirname "$d")
rm -rf "$dir"
fi
done

10
cmd/relaynode/deb.od Normal file
View File

@ -0,0 +1,10 @@
exec >&2
dir=${1%/*}
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
read -r package <"$S/$dir/package"
read -r version <"$S/oss/version/short.txt"
arch=$(dpkg --print-architecture)
redo-ifchange "$dir/${package}_$arch.deb"
rm -f "$dir/${package}"_*_"$arch.deb"
ln -sf "${package}_$arch.deb" "$dir/${package}_${version}_$arch.deb"

View File

@ -0,0 +1 @@
Tailscale IPN relay daemon.

View File

@ -0,0 +1,5 @@
redo-ifchange ../../../version/short.txt gen-changelog
(
cd ..
debian/gen-changelog
) >$3

View File

View File

@ -0,0 +1 @@
9

View File

@ -0,0 +1,14 @@
Source: tailscale-relay
Section: net
Priority: extra
Maintainer: Avery Pennarun <apenwarr@tailscale.com>
Build-Depends: debhelper (>= 10.2.5), dh-systemd (>= 1.5)
Standards-Version: 3.9.2
Homepage: https://tailscale.com/
Vcs-Git: https://github.com/tailscale/tailscale
Vcs-Browser: https://github.com/tailscale/tailscale
Package: tailscale-relay
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Traffic relay node for Tailscale IPN

View File

@ -0,0 +1,11 @@
Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=173
Upstream-Name: tailscale-relay
Upstream-Contact: Avery Pennarun <apenwarr@tailscale.com>
Source: https://github.com/tailscale/tailscale/
Files: *
Copyright: © 2019 Tailscale Inc. <info@tailscale.com>
License: Proprietary
*
* Copyright 2019 Tailscale Inc. All rights reserved.
*

View File

@ -0,0 +1,25 @@
#!/bin/sh
read junk pkgname <debian/control
read shortver <../../version/short.txt
git log --pretty='format:'"$pkgname"' (SHA:%H) unstable; urgency=low
* %s
-- %aN <%aE> %aD
' . |
python -Sc '
import os, re, subprocess, sys
first = True
def Describe(g):
global first
if first:
s = sys.argv[1]
first = False
else:
sha = g.group(1)
s = subprocess.check_output(["git", "describe", "--", sha]).strip().decode("utf-8")
return re.sub(r"^\D*", "", s)
print(re.sub(r"SHA:([0-9a-f]+)", Describe, sys.stdin.read()))
' "$shortver"

View File

@ -0,0 +1,4 @@
relaynode /usr/sbin
tailscale-login /usr/sbin
taillogin /usr/sbin
acl.json /etc/tailscale

View File

@ -0,0 +1,8 @@
#DEBHELPER#
f=/var/lib/tailscale/relay.conf
if ! [ -e "$f" ]; then
echo
echo "Note: Run tailscale-login to configure $f." >&2
echo
fi

10
cmd/relaynode/debian/rules Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/make -f
DESTDIR=debian/tailscale-relay
override_dh_auto_test:
override_dh_auto_install:
mkdir -p "${DESTDIR}/etc/default"
cp tailscale-relay.defaults "${DESTDIR}/etc/default/tailscale-relay"
%:
dh $@ --with=systemd

View File

@ -0,0 +1,12 @@
[Unit]
Description=Traffic relay node for Tailscale IPN
After=network.target
ConditionPathExists=/var/lib/tailscale/relay.conf
[Service]
EnvironmentFile=/etc/default/tailscale-relay
ExecStart=/usr/sbin/relaynode --config=/var/lib/tailscale/relay.conf --tun=wg0 $PORT $ACL_FILE $FLAGS
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,20 @@
exec >&2
dir=${1%/*}
redo-ifchange "$S/oss/version/short.txt" "$S/$dir/package" "$dir/debtmp.dir"
read -r package <"$S/$dir/package"
read -r version <"$S/oss/version/short.txt"
arch=$(dpkg --print-architecture)
(
cd "$S/$dir"
git ls-files debian | xargs redo-ifchange debian/changelog
)
cp -a "$S/$dir/debian" "$dir/debtmp/"
rm -f "$dir/debtmp/debian/$package.debhelper.log"
(
cd "$dir/debtmp" &&
debian/rules build &&
fakeroot debian/rules binary
)
mv "$dir/${package}_${version}_${arch}.deb" "$3"

View File

@ -0,0 +1,21 @@
# Generate a directory tree suitable for forming a tarball of
# this package.
exec >&2
dir=${1%/*}
outdir=$PWD/${1%.dir}
rm -rf "$outdir"
mkdir "$outdir"
touch $outdir/.stamp
sfiles="
tailscale-login
acl.json
debian/*.service
*.defaults
"
ofiles="
relaynode
../taillogin/taillogin
"
redo-ifchange "$outdir/.stamp"
(cd "$S/$dir" && redo-ifchange $sfiles && cp $sfiles "$outdir/")
(cd "$dir" && redo-ifchange $ofiles && cp $ofiles "$outdir/")

View File

@ -0,0 +1,14 @@
exec >&2
dir=${1%/*}
pkg=${1##*/}
pkg=${pkg%.rpm}
redo-ifchange "$S/oss/version/short.txt" "$dir/$pkg.tar.gz" "$dir/$pkg.spec"
read -r pkgver junk <"$S/oss/version/short.txt"
machine=$(uname -m)
rpmbase=$HOME/rpmbuild
mkdir -p "$rpmbase/SOURCES/"
cp "$dir/$pkg.tar.gz" "$rpmbase/SOURCES/"
rpmbuild -bb "$dir/$pkg.spec"
mv "$rpmbase/RPMS/$machine/$pkg-$pkgver.$machine.rpm" $3

View File

@ -0,0 +1,7 @@
redo-ifchange "$S/$1.in" "$S/oss/version/short.txt"
read -r pkgver junk <"$S/oss/version/short.txt"
basever=${pkgver%-*}
subver=${pkgver#*-}
sed -e "s/Version: 0.00$/Version: $basever/" \
-e "s/Release: 0$/Release: $subver/" \
<"$S/$1.in" >"$3"

View File

@ -0,0 +1,8 @@
exec >&2
xdir=${1%.tar.gz}
base=${xdir##*/}
updir=${xdir%/*}
redo-ifchange "$xdir.dir"
OUT="$PWD/$3"
cd "$updir" && tar -czvf "$OUT" --exclude "$base/.stamp" "$base"

15
cmd/relaynode/dist.od Normal file
View File

@ -0,0 +1,15 @@
# Build packages for customer distribution.
dir=${1%/*}
cd "$dir"
targets="tarball"
if which dh_clean fakeroot dpkg >/dev/null; then
targets="$targets deb"
else
echo "Skipping debian packages: debhelper and/or dpkg build tools missing." >&2
fi
if which rpm >/dev/null; then
targets="$targets rpm"
else
echo "Skipping rpm packages: rpm build tools missing." >&2
fi
redo-ifchange $targets

1
cmd/relaynode/docker/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/relaynode

View File

@ -0,0 +1,17 @@
# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
# Build with: docker build -t tailcontrol-alpine .
# Run with: docker run --cap-add=NET_ADMIN --device=/dev/net/tun:/dev/net/tun -it tailcontrol-alpine
FROM debian:stretch-slim
RUN apt-get update && apt-get -y install iproute2 iptables
RUN apt-get -y install ca-certificates
RUN apt-get -y install nginx-light
COPY relaynode /
# tailcontrol -tun=wg0 -dbdir=$HOME/taildb >> tailcontrol.log 2>&1 &
CMD ["/relaynode", "-R", "--config", "relay.conf"]

View File

@ -0,0 +1 @@
redo-ifchange build

View File

@ -0,0 +1,3 @@
exec >&2
redo-ifchange Dockerfile relaynode
docker build -t tailscale .

View File

@ -0,0 +1,2 @@
redo-ifchange ../relaynode
cp ../relaynode $3

10
cmd/relaynode/docker/run.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
set -e
redo-ifchange build
docker run --cap-add=NET_ADMIN \
--device=/dev/net/tun:/dev/net/tun \
-it tailscale

1
cmd/relaynode/package Normal file
View File

@ -0,0 +1 @@
tailscale-relay

300
cmd/relaynode/relaynode.go Normal file
View File

@ -0,0 +1,300 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Relaynode is the old Linux Tailscale daemon.
//
// Deprecated: this program will be soon deleted. The replacement is
// cmd/tailscaled.
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/apenwarr/fixconsole"
"github.com/google/go-cmp/cmp"
"github.com/klauspost/compress/zstd"
"github.com/pborman/getopt/v2"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/atomicfile"
"tailscale.com/control/controlclient"
"tailscale.com/control/policy"
"tailscale.com/logpolicy"
"tailscale.com/version"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
)
func main() {
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Printf("fixConsoleOutput: %v\n", err)
}
config := getopt.StringLong("config", 'f', "", "path to config file")
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server")
listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)")
tunname := getopt.StringLong("tun", 0, "wg0", "tunnel interface name")
alwaysrefresh := getopt.BoolLong("always-refresh", 0, "force key refresh at startup")
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes")
rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes")
droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node")
routes := getopt.StringLong("routes", 0, "", "list of IP ranges this node can relay")
aclfile := getopt.StringLong("acl-file", 0, "", "restrict traffic relaying according to json ACL file")
derp := getopt.BoolLong("derp", 0, "enable bypass via Detour Encrypted Routing Protocol (DERP)", "false")
debug := getopt.StringLong("debug", 0, "", "Address of debug server")
getopt.Parse()
if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
}
uflags := controlclient.UFlagsHelper(!*nuroutes, *rroutes, *droutes)
if *config == "" {
log.Fatal("no --config file specified")
}
if *tunname == "" {
log.Printf("Warning: no --tun device specified; routing disabled.\n")
}
pol := logpolicy.New("tailnode.log.tailscale.io", *config)
logf := wgengine.RusagePrefixLog(log.Printf)
// The wgengine takes a wireguard configuration produced by the
// controlclient, and runs the actual tunnels and packets.
var e wgengine.Engine
if *fake {
e, err = wgengine.NewFakeUserspaceEngine(logf, *listenport, *derp)
} else {
e, err = wgengine.NewUserspaceEngine(logf, *tunname, *listenport, *derp)
}
if err != nil {
log.Fatalf("Error starting wireguard engine: %v\n", err)
}
e = wgengine.NewWatchdog(e)
var lastacljson string
var p *policy.Policy
if *aclfile == "" {
e.SetFilter(nil)
} else {
lastacljson = readOrFatal(*aclfile)
p = installFilterOrFatal(e, *aclfile, lastacljson, nil)
}
var lastNetMap *controlclient.NetworkMap
var lastUserMap map[string][]filter.IP
statusFunc := func(new controlclient.Status) {
if new.URL != "" {
fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
return
}
if new.Err != "" {
log.Print(new.Err)
return
}
if new.Persist != nil {
if err := saveConfig(*config, *new.Persist); err != nil {
log.Println(err)
}
}
if m := new.NetMap; m != nil {
if lastNetMap != nil {
s1 := strings.Split(lastNetMap.Concise(), "\n")
s2 := strings.Split(new.NetMap.Concise(), "\n")
logf("netmap diff:\n%v\n", cmp.Diff(s1, s2))
}
lastNetMap = m
if m.Equal(&controlclient.NetworkMap{}) {
return
}
wgcfg, err := m.WGCfg(uflags, m.DNS)
if err != nil {
log.Fatalf("Error getting wg config: %v\n", err)
}
err = e.Reconfig(wgcfg, m.DNSDomains)
if err != nil {
log.Fatalf("Error reconfiguring engine: %v\n", err)
}
lastUserMap = m.UserMap()
if p != nil {
matches, err := p.Expand(lastUserMap)
if err != nil {
log.Fatalf("Error expanding ACLs: %v\n", err)
}
e.SetFilter(filter.New(matches))
}
}
}
cfg, err := loadConfig(*config)
if err != nil {
log.Fatal(err)
}
hi := controlclient.NewHostinfo()
hi.FrontendLogID = pol.PublicID.String()
hi.BackendLogID = pol.PublicID.String()
if *routes != "" {
for _, routeStr := range strings.Split(*routes, ",") {
cidr, err := wgcfg.ParseCIDR(routeStr)
if err != nil {
log.Fatalf("--routes: not an IP range: %s", routeStr)
}
hi.RoutableIPs = append(hi.RoutableIPs, *cidr)
}
}
c, err := controlclient.New(controlclient.Options{
Persist: cfg,
ServerURL: *server,
Hostinfo: &hi,
NewDecompressor: func() (controlclient.Decompressor, error) {
return zstd.NewReader(nil)
},
KeepAlive: true,
})
c.SetStatusFunc(statusFunc)
if err != nil {
log.Fatal(err)
}
lf := controlclient.LoginDefault
if *alwaysrefresh {
lf |= controlclient.LoginInteractive
}
c.Login(nil, lf)
// Print the wireguard status when we get an update.
e.SetStatusCallback(func(s *wgengine.Status, err error) {
if err != nil {
log.Fatalf("Wireguard engine status error: %v\n", err)
}
var ss []string
for _, p := range s.Peers {
if p.LastHandshake.IsZero() {
ss = append(ss, "x")
} else {
ss = append(ss, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
}
}
logf("v%v peers: %v\n", version.LONG, strings.Join(ss, " "))
c.UpdateEndpoints(0, s.LocalAddrs)
})
if *debug != "" {
go runDebugServer(*debug)
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
signal.Notify(sigCh, syscall.SIGTERM)
t := time.NewTicker(5 * time.Second)
loop:
for {
select {
case <-t.C:
// For the sake of curiosity, request a status
// update periodically.
e.RequestStatus()
// check if aclfile has changed.
// TODO(apenwarr): use fsnotify instead of polling?
if *aclfile != "" {
json := readOrFatal(*aclfile)
if json != lastacljson {
logf("ACL file (%v) changed. Reloading filter.\n", *aclfile)
lastacljson = json
p = installFilterOrFatal(e, *aclfile, json, lastUserMap)
}
}
case <-sigCh:
logf("signal received, exiting")
t.Stop()
break loop
}
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
e.Close()
pol.Shutdown(ctx)
}
func loadConfig(path string) (cfg controlclient.Persist, err error) {
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
log.Printf("config %s does not exist", path)
return controlclient.Persist{}, nil
}
if err := json.Unmarshal(b, &cfg); err != nil {
return controlclient.Persist{}, fmt.Errorf("load config: %v", err)
}
return cfg, nil
}
func saveConfig(path string, cfg controlclient.Persist) error {
b, err := json.MarshalIndent(cfg, "", "\t")
if err != nil {
return fmt.Errorf("save config: %v", err)
}
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
return fmt.Errorf("save config: %v", err)
}
return nil
}
func readOrFatal(filename string) string {
b, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("%v: ReadFile: %v\n", filename, err)
}
return string(b)
}
func installFilterOrFatal(e wgengine.Engine, filename, acljson string, usermap map[string][]filter.IP) *policy.Policy {
p, err := policy.Parse(acljson)
if err != nil {
log.Fatalf("%v: json filter: %v\n", filename, err)
}
matches, err := p.Expand(usermap)
if err != nil {
log.Fatalf("%v: json filter: %v\n", filename, err)
}
e.SetFilter(filter.New(matches))
return p
}
func runDebugServer(addr string) {
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
srv := http.Server{
Addr: addr,
Handler: mux,
}
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

9
cmd/relaynode/rpm.od Normal file
View File

@ -0,0 +1,9 @@
exec >&2
dir=${2%/*}
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
read -r package <"$S/$dir/package"
read -r pkgver <"$S/oss/version/short.txt"
machine=$(uname -m)
redo-ifchange "$dir/$package.rpm"
rm -f "$dir/${package}"-*."$machine.rpm"
ln -sf "$package.rpm" "$dir/$package-$pkgver.$machine.rpm"

4
cmd/relaynode/tailscale-login Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
cfg=/var/lib/tailscale/relay.conf
dir=$(dirname "$0")
"$dir/taillogin" --config="$cfg"

View File

@ -0,0 +1,14 @@
# Set the port to listen on for incoming VPN packets.
# Remote nodes will automatically be informed about the new port number,
# but you might want to configure this in order to set external firewall
# settings.
PORT="--port=41641"
# Comment out this line to allow all traffic to be relayed.
# Or edit the given file to allow specific traffic.
# The example file is unlikely to match any users on your network, so it
# will block all incoming traffic by default.
ACL_FILE="--acl-file=/etc/tailscale/acl.json"
# Extra flags you might want to pass to relaynode.
FLAGS=""

View File

@ -0,0 +1,42 @@
Name: tailscale-relay
Version: 0.00
Release: 0
Summary: Traffic relay node for Tailscale
Group: Network
License: Proprietary
URL: https://tailscale.com/
Vendor: Tailscale Inc.
#Source: https://github.com/tailscale/tailscale
Source0: tailscale-relay.tar.gz
#Prefix: %{_prefix}
Packager: Avery Pennarun <apenwarr@tailscale.com>
BuildRoot: %{_tmppath}/%{name}-root
%description
Traffic relay node for Tailscale.
%prep
%setup -n tailscale-relay
%build
%install
D=$RPM_BUILD_ROOT
[ "$D" = "/" -o -z "$D" ] && exit 99
rm -rf "$D"
mkdir -p $D/usr/sbin $D/lib/systemd/system $D/etc/default $D/etc/tailscale
cp taillogin tailscale-login relaynode $D/usr/sbin
cp tailscale-relay.service $D/lib/systemd/system/
cp tailscale-relay.defaults $D/etc/default/tailscale-relay
cp acl.json $D/etc/tailscale/acl.json
%clean
%files
%defattr(-,root,root)
%config(noreplace) /etc/default/tailscale-relay
%config(noreplace) /etc/tailscale/acl.json
/lib/systemd/system/tailscale-relay.service
/usr/sbin/taillogin
/usr/sbin/tailscale-login
/usr/sbin/relaynode

7
cmd/relaynode/tarball.od Normal file
View File

@ -0,0 +1,7 @@
dir=${1%/*}
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
read -r package <"$S/$dir/package"
read -r version <"$S/oss/version/short.txt"
redo-ifchange "$dir/$package.tar.gz"
rm -f "$dir/$package"-*.tar.gz
ln -sf "$package.tar.gz" "$dir/$package-$version.tar.gz"