Open-Source Licences
Last updated: 5 October 2026
Skimmo’s proxy engine is built on Xray-core, licensed under the Mozilla Public License 2.0, and on the open-source libraries it depends on. The full list, with each component’s licence text, is on the app’s Settings → About → Open-source licences screen. Skimmo modifies two of those components; both changes are published in full below.
Xray-core
Skimmo builds Xray-core from the pinned upstream version (v1.260327.1-0.20260908222543-52a412d9e2f5, the
v26.9.9 release commit) with one change. Upstream’s Shadowsocks 2022 support is built on two GPL-3.0 libraries
(sagernet/sing and sagernet/sing-shadowsocks). Skimmo’s build runs the Shadowsocks 2022
client on go-shadowsocks2 (Apache-2.0) instead, and leaves out the
Shadowsocks 2022 server side, which only those libraries implement, so Skimmo contains no GPL/AGPL code.
The Mozilla Public License is file-level copyleft: modifying an MPL file means the source of that
modification must be made available. This is that publication: the patch below, against the upstream version
above, is the complete Source Code Form of the modification. Before applying it, the build deletes
proxy/shadowsocks_2022/inbound.go, inbound_multi.go, inbound_relay.go and
common/singbridge.
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@
go 1.27
require (
+ github.com/go-gost/go-shadowsocks2 v0.1.4
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
@@ -18,8 +19,6 @@
github.com/pires/go-proxyproto v0.15.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
- github.com/sagernet/sing v0.5.1
- github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
--- a/infra/conf/shadowsocks.go
+++ b/infra/conf/shadowsocks.go
@@ -3,8 +3,6 @@
import (
"strings"
- "github.com/sagernet/sing-shadowsocks/shadowaead_2022"
- C "github.com/sagernet/sing/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
@@ -14,6 +12,15 @@
"google.golang.org/protobuf/proto"
)
+// isShadowsocks2022 reports whether the method is one of the Shadowsocks 2022 ciphers. Upstream's
+// implementation pulls in the GPL-3 sagernet/sing modules; this build's client runs on
+// go-shadowsocks2 (Apache-2.0) instead (see proxy/shadowsocks_2022/outbound.go), and has no server.
+func isShadowsocks2022(cipher string) bool {
+ return strings.HasPrefix(strings.ToLower(cipher), "2022-blake3-")
+}
+
+var errShadowsocks2022Server = errors.New("Shadowsocks 2022 servers are not supported in this build")
+
func cipherFromString(c string) shadowsocks.CipherType {
switch strings.ToLower(c) {
case "aes-128-gcm", "aead_aes_128_gcm":
@@ -55,8 +62,8 @@
v.Users = v.Clients
}
- if C.Contains(shadowaead_2022.List, v.Cipher) {
- return buildShadowsocks2022(v)
+ if isShadowsocks2022(v.Cipher) {
+ return nil, errShadowsocks2022Server
}
config := new(shadowsocks.ServerConfig)
@@ -110,72 +117,6 @@
return config, nil
}
-func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
- if len(v.Users) == 0 {
- config := new(shadowsocks_2022.ServerConfig)
- config.Method = v.Cipher
- config.Key = v.Password
- config.Network = v.NetworkList.Build()
- config.Email = v.Email
- return config, nil
- }
-
- if v.Cipher == "" {
- return nil, errors.New("shadowsocks 2022 (multi-user): missing server method")
- }
- if !strings.Contains(v.Cipher, "aes") {
- return nil, errors.New("shadowsocks 2022 (multi-user): only blake3-aes-*-gcm methods are supported")
- }
-
- if v.Users[0].Address == nil {
- config := new(shadowsocks_2022.MultiUserServerConfig)
- config.Method = v.Cipher
- config.Key = v.Password
- config.Network = v.NetworkList.Build()
-
- config.Users = make([]*protocol.User, len(v.Users))
- processUser := func(idx int) error {
- user := v.Users[idx]
- if user.Cipher != "" {
- return errors.New("shadowsocks 2022 (multi-user): users must have empty method")
- }
- account := &shadowsocks_2022.Account{
- Key: user.Password,
- }
- config.Users[idx] = &protocol.User{
- Email: user.Email,
- Level: uint32(user.Level),
- Account: serial.ToTypedMessage(account),
- }
- return nil
- }
- if err := task.ParallelForN(len(v.Users), processUser); err != nil {
- return nil, err
- }
- return config, nil
- }
-
- config := new(shadowsocks_2022.RelayServerConfig)
- config.Method = v.Cipher
- config.Key = v.Password
- config.Network = v.NetworkList.Build()
- for _, user := range v.Users {
- if user.Cipher != "" {
- return nil, errors.New("shadowsocks 2022 (relay): users must have empty method")
- }
- if user.Address == nil {
- return nil, errors.New("shadowsocks 2022 (relay): all users must have relay address")
- }
- config.Destinations = append(config.Destinations, &shadowsocks_2022.RelayDestination{
- Key: user.Password,
- Email: user.Email,
- Address: user.Address.Build(),
- Port: uint32(user.Port),
- })
- }
- return config, nil
-}
-
type ShadowsocksServerTarget struct {
Address *Address `json:"address"`
Port uint16 `json:"port"`
@@ -214,31 +155,31 @@
return nil, errors.New(`Shadowsocks settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Shadowsocks outbounds and routing balancer instead`)
}
- if len(v.Servers) == 1 {
+ if len(v.Servers) == 1 && isShadowsocks2022(v.Servers[0].Cipher) {
server := v.Servers[0]
- if C.Contains(shadowaead_2022.List, server.Cipher) {
- if server.Address == nil {
- return nil, errors.New("Shadowsocks server address is not set.")
- }
- if server.Port == 0 {
- return nil, errors.New("Invalid Shadowsocks port.")
- }
- if server.Password == "" {
- return nil, errors.New("Shadowsocks password is not specified.")
- }
-
- config := new(shadowsocks_2022.ClientConfig)
- config.Address = server.Address.Build()
- config.Port = uint32(server.Port)
- config.Method = server.Cipher
- config.Key = server.Password
- return config, nil
+ if !shadowsocks_2022.IsSupportedMethod(server.Cipher) {
+ return nil, errors.New("Shadowsocks 2022 method ", server.Cipher, " is not supported in this build")
}
+ if server.Address == nil {
+ return nil, errors.New("Shadowsocks server address is not set.")
+ }
+ if server.Port == 0 {
+ return nil, errors.New("Invalid Shadowsocks port.")
+ }
+ if server.Password == "" {
+ return nil, errors.New("Shadowsocks password is not specified.")
+ }
+ config := new(shadowsocks_2022.ClientConfig)
+ config.Address = server.Address.Build()
+ config.Port = uint32(server.Port)
+ config.Method = server.Cipher
+ config.Key = server.Password
+ return config, nil
}
config := new(shadowsocks.ClientConfig)
for _, server := range v.Servers {
- if C.Contains(shadowaead_2022.List, server.Cipher) {
+ if isShadowsocks2022(server.Cipher) {
return nil, errors.New("Shadowsocks 2022 accept no multi servers")
}
if server.Address == nil {
--- a/proxy/shadowsocks_2022/outbound.go
+++ b/proxy/shadowsocks_2022/outbound.go
@@ -1,22 +1,30 @@
package shadowsocks_2022
+// Skimmo change (2026-09-27): this outbound is rewritten on top of
+// github.com/go-gost/go-shadowsocks2/shadowaead_2022 (Apache-2.0) instead of
+// github.com/sagernet/sing-shadowsocks (GPL-3.0), so the build carries no GPL code. Client only;
+// the server-side handlers (inbound*.go) are not part of this build. The TCP/UDP copy loops follow
+// Xray's own proxy/shadowsocks client.
+
import (
"context"
- "time"
+ "crypto/rand"
+ "encoding/binary"
+ "sync"
+ "sync/atomic"
- shadowsocks "github.com/sagernet/sing-shadowsocks"
- "github.com/sagernet/sing-shadowsocks/shadowaead_2022"
- C "github.com/sagernet/sing/common"
- B "github.com/sagernet/sing/common/buf"
- "github.com/sagernet/sing/common/bufio"
- N "github.com/sagernet/sing/common/network"
+ "github.com/go-gost/go-shadowsocks2/core"
+ shadowaead_2022 "github.com/go-gost/go-shadowsocks2/shadowaead_2022"
+ "github.com/go-gost/go-shadowsocks2/socks"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal"
- "github.com/xtls/xray-core/common/singbridge"
+ "github.com/xtls/xray-core/common/task"
+ xcore "github.com/xtls/xray-core/core"
+ "github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
)
@@ -27,43 +35,50 @@
}))
}
+// Methods this build can run. 2022-blake3-chacha20-poly1305 is left out: go-shadowsocks2 does not
+// implement it (its UDP form is a different packet layout, XChaCha20 with no separate header).
+var methods = map[string]core.MetaCipher{
+ "2022-blake3-aes-128-gcm": {Name: "2022-blake3-aes-128-gcm", KeySize: 16, SaltSize: 16, NonceSize: 12, TagSize: 16},
+ "2022-blake3-aes-256-gcm": {Name: "2022-blake3-aes-256-gcm", KeySize: 32, SaltSize: 32, NonceSize: 12, TagSize: 16},
+}
+
+// IsSupportedMethod reports whether this build's Shadowsocks 2022 client can run method.
+func IsSupportedMethod(method string) bool {
+ _, ok := methods[method]
+ return ok
+}
+
type Outbound struct {
- ctx context.Context
- server net.Destination
- method shadowsocks.Method
+ server net.Destination
+ cipher core.ShadowCipher
+ policyManager policy.Manager
}
func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
- o := &Outbound{
- ctx: ctx,
+ meta, ok := methods[config.Method]
+ if !ok {
+ return nil, errors.New("unsupported Shadowsocks 2022 method ", config.Method)
+ }
+ if config.Key == "" {
+ return nil, errors.New("missing psk")
+ }
+ // The key is the base64 PSK, or "iPSK:uPSK" for a relay (extensible identity headers).
+ cipher, err := shadowaead_2022.AESGCM(meta, config.Key)
+ if err != nil {
+ return nil, errors.New("create method").Base(err)
+ }
+ v := xcore.MustFromContext(ctx)
+ return &Outbound{
server: net.Destination{
Address: config.Address.AsAddress(),
Port: net.Port(config.Port),
- Network: net.Network_TCP,
},
- }
- if C.Contains(shadowaead_2022.List, config.Method) {
- if config.Key == "" {
- return nil, errors.New("missing psk")
- }
- method, err := shadowaead_2022.NewWithPassword(config.Method, config.Key, nil)
- if err != nil {
- return nil, errors.New("create method").Base(err)
- }
- o.method = method
- } else {
- return nil, errors.New("unknown method ", config.Method)
- }
- return o, nil
+ cipher: cipher,
+ policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
+ }, nil
}
func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
- var inboundConn net.Conn
- inbound := session.InboundFromContext(ctx)
- if inbound != nil {
- inboundConn = inbound.Conn
- }
-
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
if !ob.Target.IsValid() {
@@ -72,76 +87,197 @@
ob.Name = "shadowsocks-2022"
ob.CanSpliceCopy = 3
destination := ob.Target
- network := destination.Network
- errors.LogInfo(ctx, "tunneling request to ", destination, " via ", o.server.NetAddr())
-
serverDestination := o.server
- serverDestination.Network = network
- connection, err := dialer.Dial(ctx, serverDestination)
+ serverDestination.Network = destination.Network
+ conn, err := dialer.Dial(ctx, serverDestination)
if err != nil {
return errors.New("failed to connect to server").Base(err)
}
- defer connection.Close()
+ defer conn.Close()
+ errors.LogInfo(ctx, "tunneling request to ", destination, " via ", serverDestination)
+ var newCtx context.Context
+ var newCancel context.CancelFunc
if session.TimeoutOnlyFromContext(ctx) {
- ctx, _ = context.WithCancel(context.Background())
+ newCtx, newCancel = context.WithCancel(context.Background())
}
+ sessionPolicy := o.policyManager.ForLevel(0)
+ ctx, cancel := context.WithCancel(ctx)
+ timer := signal.CancelAfterInactivity(ctx, func() {
+ cancel()
+ if newCancel != nil {
+ newCancel()
+ }
+ }, sessionPolicy.Timeouts.ConnectionIdle)
+ if newCtx != nil {
+ ctx = newCtx
+ }
- if network == net.Network_TCP {
- serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination))
- var handshake bool
- if timeoutReader, isTimeoutReader := link.Reader.(buf.TimeoutReader); isTimeoutReader {
- mb, err := timeoutReader.ReadMultiBufferTimeout(time.Millisecond * 100)
- if err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
- return errors.New("read payload").Base(err)
- }
- payload := B.New()
- for {
- payload.Reset()
- nb, n := buf.SplitBytes(mb, payload.FreeBytes())
- if n > 0 {
- payload.Truncate(n)
- _, err = serverConn.Write(payload.Bytes())
- if err != nil {
- payload.Release()
- return errors.New("write payload").Base(err)
- }
- handshake = true
- }
- if nb.IsEmpty() {
- break
- }
- mb = nb
- }
- payload.Release()
+ var requestDone, responseDone func() error
+ if destination.Network == net.Network_TCP {
+ target := socks.ParseAddr(destination.NetAddr())
+ if target == nil {
+ return errors.New("invalid target ", destination)
}
- if !handshake {
- _, err = serverConn.Write(nil)
- if err != nil {
- return errors.New("client handshake").Base(err)
- }
+ client := core.NewTCPClient(core.ClientConfig{Cipher: o.cipher})
+ ssConn, err := client.WrapConn(conn, target)
+ if err != nil {
+ return errors.New("client handshake").Base(err)
}
- return singbridge.CopyConn(ctx, inboundConn, link, serverConn)
+ requestDone = func() error {
+ defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
+ return buf.Copy(link.Reader, buf.NewWriter(ssConn), buf.UpdateActivity(timer))
+ }
+ responseDone = func() error {
+ defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
+ return buf.Copy(buf.NewReader(ssConn), link.Writer, buf.UpdateActivity(timer))
+ }
} else {
- var packetConn N.PacketConn
- if pc, isPacketConn := inboundConn.(N.PacketConn); isPacketConn {
- packetConn = pc
- } else if nc, isNetPacket := inboundConn.(net.PacketConn); isNetPacket {
- packetConn = bufio.NewPacketConn(nc)
- } else {
- packetConn = &singbridge.PacketConnWrapper{
- Reader: link.Reader,
- Writer: link.Writer,
- Conn: inboundConn,
- Dest: destination,
- T: signal.CancelAfterInactivity(ctx, func() {
- common.Interrupt(link.Reader)
- }, 300*time.Second),
- }
+ udp := &udpSession{cipher: o.cipher, conn: conn, target: destination}
+ if err := udp.init(); err != nil {
+ return err
}
+ requestDone = func() error {
+ defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
+ return buf.Copy(link.Reader, udp, buf.UpdateActivity(timer))
+ }
+ responseDone = func() error {
+ defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
+ return buf.Copy(udp, link.Writer, buf.UpdateActivity(timer))
+ }
+ }
- serverConn := o.method.DialPacketConn(connection)
- return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
+ responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
+ if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
+ return errors.New("connection ends").Base(err)
}
+ return nil
}
+
+// udpSession is one SIP022 UDP session: a random session ID and a packet counter, on one socket to
+// the server. Each packet carries its own target, so a flow that talks to several destinations works;
+// replies are matched to this session by their client session ID and keep the source address the
+// server reports, except that a reply to a domain target is tagged with that domain (see
+// replySource).
+type udpSession struct {
+ cipher core.ShadowCipher
+ conn net.Conn
+ target net.Destination
+ sessionID uint64
+ packetID atomic.Uint64
+
+ mu sync.Mutex
+ sentIP map[string]bool // IP targets written to, by "ip:port"
+ sentByDom map[net.Port]net.Destination // last domain target written to, by port
+}
+
+func (s *udpSession) init() error {
+ var b [8]byte
+ if _, err := rand.Read(b[:]); err != nil {
+ return err
+ }
+ s.sessionID = binary.BigEndian.Uint64(b[:])
+ return nil
+}
+
+// WriteMultiBuffer implements buf.Writer: encrypt and send each packet.
+func (s *udpSession) WriteMultiBuffer(mb buf.MultiBuffer) error {
+ defer buf.ReleaseMulti(mb)
+ eih := len(s.cipher.Keys()) > 1
+ for _, b := range mb {
+ if b == nil {
+ continue
+ }
+ dest := s.target
+ if b.UDP != nil {
+ dest = *b.UDP
+ }
+ target := socks.ParseAddr(dest.NetAddr())
+ if target == nil {
+ return errors.New("invalid UDP target ", dest)
+ }
+ s.remember(dest)
+ packet, err := shadowaead_2022.PackUDP(s.cipher, eih, s.cipher.FirstKey(), s.cipher.Key(),
+ b.Bytes(), target, s.sessionID, s.packetID.Add(1)-1, 0)
+ if err != nil {
+ return errors.New("encrypt UDP packet").Base(err)
+ }
+ if _, err := s.conn.Write(packet); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ReadMultiBuffer implements buf.Reader: receive, decrypt, and tag each payload with its source.
+func (s *udpSession) ReadMultiBuffer() (buf.MultiBuffer, error) {
+ packet := make([]byte, 65535)
+ for {
+ n, err := s.conn.Read(packet)
+ if err != nil {
+ return nil, err
+ }
+ _, header, payload, _, err := shadowaead_2022.UnpackUDP(s.cipher, nil, packet[:n])
+ if err != nil {
+ errors.LogDebugInner(context.Background(), err, "drop undecryptable UDP packet")
+ continue
+ }
+ if header.Type != shadowaead_2022.HeaderTypeServerStream || header.ClientSessionID != s.sessionID {
+ continue // not a reply to this session
+ }
+ source, err := net.ParseDestination("udp:" + header.Address.String())
+ if err != nil {
+ source = s.target
+ }
+ source = s.replySource(source)
+ b := buf.New()
+ if len(payload) > buf.Size {
+ b.Release()
+ b = buf.NewWithSize(int32(len(payload)))
+ }
+ if _, err := b.Write(payload); err != nil {
+ b.Release()
+ return nil, err
+ }
+ b.UDP = &source
+ return buf.MultiBuffer{b}, nil
+ }
+}
+
+func (s *udpSession) remember(dest net.Destination) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if dest.Address.Family().IsDomain() {
+ if s.sentByDom == nil {
+ s.sentByDom = make(map[net.Port]net.Destination)
+ }
+ s.sentByDom[dest.Port] = dest
+ return
+ }
+ if s.sentIP == nil {
+ s.sentIP = make(map[string]bool)
+ }
+ s.sentIP[dest.NetAddr()] = true
+}
+
+// replySource maps a reply's source back to the domain the packet was sent to. The server resolves a
+// domain target and reports the IP it got a reply from, but the flow asked for the domain (e.g. a
+// FakeDNS address sniffed back to one), and Xray only rewrites replies from the requested address
+// back to the address the app used. Without this, a UDP reply to a domain target comes back from an
+// address the app never sent to and is dropped. Like freedom's UDPOverride: a reply from an IP we
+// never sent to, on a port we sent a domain packet to, is from that domain.
+func (s *udpSession) replySource(source net.Destination) net.Destination {
+ if source.Address.Family().IsDomain() {
+ return source
+ }
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.sentIP[source.NetAddr()] {
+ return source
+ }
+ if d, ok := s.sentByDom[source.Port]; ok {
+ return d
+ }
+ return source
+}
go-shadowsocks2
go-shadowsocks2 v0.1.4 (Apache-2.0) derives the Shadowsocks 2022 UDP session key from the session ID in little-endian byte order, where the specification and other implementations use big-endian, so its UDP traffic could not be decrypted by other servers. Skimmo fixes the byte order:
--- a/shadowaead_2022/packet.go
+++ b/shadowaead_2022/packet.go
@@ -223,7 +223,10 @@
// Format: AES(separate_header) + AEAD(body)
func PackUDP(c core.ShadowCipher, eih bool, separateHeaderKey, bodyKey []byte, payload []byte, target socks.Addr, sessionID, packetID, clientSessionID uint64) ([]byte, error) {
salt := make([]byte, 8)
- binary.LittleEndian.PutUint64(salt, sessionID)
+ // Skimmo change (2026-09-27): the session ID is serialised big-endian here, as in the separate
+ // header and in SIP022; upstream v0.1.4 used little-endian, so UDP failed to authenticate against
+ // every other SS2022 implementation (sing-shadowsocks / Xray, shadowsocks-rust).
+ binary.BigEndian.PutUint64(salt, sessionID)
// Derive session subkey from salt for AEAD
aead, err := c.Encrypter(bodyKey, salt)
@@ -364,7 +367,10 @@
func decryptPayload(ciph core.ShadowCipher, decryptKey []byte, separateHeader *SeparateHeader, separateHeaderEncoding []byte, encryptedBody []byte) (*UDPHeader, []byte, error) {
salt := make([]byte, 8)
- binary.LittleEndian.PutUint64(salt, separateHeader.SessionID)
+ // Skimmo change (2026-09-27): the session ID is serialised big-endian here, as in the separate
+ // header and in SIP022; upstream v0.1.4 used little-endian, so UDP failed to authenticate against
+ // every other SS2022 implementation (sing-shadowsocks / Xray, shadowsocks-rust).
+ binary.BigEndian.PutUint64(salt, separateHeader.SessionID)
// Derive session subkey from salt for AEAD
aead, err := ciph.Decrypter(decryptKey, salt)
Routing data bundled with the app
The “Bypass China” and “Blacklist” modes use two public databases. Skimmo ships a copy of each in the app and downloads newer releases of the same files; both are used unmodified.
geosite.datfrom v2fly/domain-list-community, © 2018–2019 V2Ray, under the MIT License.geoip.dat(geoip-only-cn-private.dat) from v2fly/geoip by the V2Fly project, under CC BY-SA 4.0. It is built from IP Geolocation by DB-IP, under CC BY 4.0.
The app’s licences screen also lists the Go standard library and runtime (BSD-3-Clause, © The Go Authors), which is compiled into the engine.
Typeface
The app’s text and the skimmo wordmark use Outfit, © 2021 The Outfit Project Authors, under the SIL Open Font License 1.1; the full licence is on the app’s licences screen.